在Linux网络编程中,Socket是实现进程间通信的基石。无论你用C++、Python还是Go,理解UDP与TCP的Socket接口都是构建高并发网络服务的必经之路。本文将从零开始,带你用C++封装UDP和TCP服务器,并深入探讨多客户端场景下的优化方向。

一、UDP编程:无连接的高效传输

UDP(User Datagram Protocol)是一种无连接的传输层协议,它不保证数据可靠到达,但胜在开销小、延迟低。在实时音视频、DNS查询等场景中,UDP往往是首选。对于习惯Java或Python的开发者来说,C++的Socket接口更接近系统底层,理解起来需要一点耐心,但掌握后能让你对网络通信有更本质的认识。

1.1 核心接口一览

socket() – 创建套接字

调用 socket() 函数可以创建一个新的套接字,返回一个文件描述符。

#include 
int socket(int domain, int type, int protocol);

参数说明:

  • domain:协议族,常用 AF_INET(IPv4)、AF_INET6(IPv6)或 AF_UNIX(本地通信)。
  • type:套接字类型,常用 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP)。
  • protocol:通常设为0,由系统自动选择协议。

返回值:成功返回非负文件描述符,失败返回-1并设置 errno。

关键理解:socket返回的文件描述符指向一个内核缓冲区,往该缓冲区写入的数据会被协议栈发送到远端。因此,Socket编程本质上是进程间通信的一种形式。

bind() – 绑定地址和端口

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

该函数将套接字与本地IP和端口绑定。服务端必须显式调用bind,客户端通常由系统自动分配端口。参数包括:

  • sockfd:socket返回的描述符。
  • addr:指向 sockaddr_in 或 sockaddr_in6 结构的指针。
  • addrlen:地址结构长度。

返回0表示成功,-1表示失败。

 是一个通用的地址结构体,它本身不直接使用,而是作为 、、 等函数的参数类型,允许这些函数接收不同协议族的地址结构。

但使用的时候一般是sockaddr_in

  • 大小:16 字节(与  相同,便于类型转换)。

  • 使用时先填充 ,然后强制转换为  传递给函数。

send() / write() – 数据发送

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

发送数据,flags 通常为0。成功返回发送字节数,失败返回-1。

recv() / read() – 数据接收

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

接收数据,成功返回接收字节数(0表示对方关闭连接),失败返回-1。

1.2 C++封装UDPServer

为了代码复用和可读性,我们用C++对上述接口进行封装,实现一个简易的UdpServer类。

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#define DEFAULT_PORT 8080
#define MAXNUM 1024
class UdpServer
{
public:
    UdpServer(uint16_t port = DEFAULT_PORT)//指定本地端口号
    :_port(port)
    {}
    void init()//初始化->1.创建套接字 2.绑定套接字
    {
        //创建套接字
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
            exit(1);
        //初始化sockaddr_i
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        //绑定套接字
        int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n != 0)
            exit(1);
    }
    void start()//服务器启动
    {
        char buffer[MAXNUM];
        //客户端的信息
        struct sockaddr_in peer;
        socklen_t len = 0;
        while(1)
        {
            char buffer[MAXNUM];
            //接受数据
            int n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&peer,&len);
            if(n > 0)
            {
                buffer[n] = '\0';
                std::cout<

1.3 客户端与服务器构建

客户端启动时需要知道服务器的IP和端口。如果在本机测试,可以填 127.0.0.1(本地环回地址)。客户端和服务器都需要创建套接字,但只有服务器需要显式bind。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main(int argc,char* argv[])//指定服务器的ip地址和端口号
{
    if(argc != 3)
    {
        std::cout<<"format:"<<__FILE__<<" ip port"<";
        std::string s;std::getline(std::cin,s);
        ssize_t n = sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&sock,sizeof(sock));
    }
    return 0;
}

服务器端则需指定本地端口号:

#include "server.hpp"
int main(int argv,char* argc[])
{
    if(argv != 2)
    {
        std::cout<<"format: "<<__FILE__<<" port"<

测试效果如下:

[AFFILIATE_SLOT_1]

二、TCP编程:面向连接的可靠通信

TCP编程的接口与UDP有相似之处,但多了监听和接受连接的步骤。服务器绑定套接字后,需要调用listen进入监听状态,当客户端发起连接时,通过accept接受连接并返回一个新的套接字用于通信。这个新的套接字才代表与具体客户端的连接,而原始套接字继续用于监听。

2.1 常用接口

listen() – 监听连接

int listen(int sockfd, int backlog);

将套接字转为被动监听状态。参数 sockfd 为已绑定的监听套接字,backlog 指定未完成连接队列的最大长度(通常5~128)。成功返回0,失败返回-1。

accept() – 接受连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

从监听套接字的完成连接队列中取出第一个连接,并创建新的套接字用于通信。参数:

  • sockfd:监听套接字。
  • addr:输出参数,返回客户端地址信息(可为NULL)。
  • addrlen:输入输出参数,传入地址结构长度,返回实际长度。

成功返回新的连接套接字描述符,失败返回-1。

connect() – 发起连接(客户端)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端主动连接服务器。参数包括客户端套接字、服务器地址结构及其长度。成功返回0,失败返回-1。

2.2 C++封装InetAddr

网络编程中频繁进行字节序转换和sockaddr_in操作,我们设计一个InetAddr类来简化这些工作。它同时保存网络字节序的sockaddr_in和本地字节序的IP与端口。

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
class InetAddr{
public:
    InetAddr(struct sockaddr_in & addr)//网络字节序转主机字节序
    :_addr(addr)
    {
        _port = ntohs(addr.sin_port);
        char buffer[64];
        inet_ntop(AF_INET,&addr.sin_addr,buffer,sizeof(_addr));
        _ip = buffer;
    }
    InetAddr(std::string ip,uint16_t port)//主机字节序转网络字节序
    :_port(port),_ip(ip)
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);
    }
    InetAddr(uint16_t port)//默认本地环回
    :_port(port),_ip("00.00.00.00")
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    const struct sockaddr_in& get_addr()
    {
        return _addr;
    }
    struct sockaddr_in* get_addr_ptr()
    {
        return &_addr;
    }
    std::string get_string()
    {
        return _ip + ":" + std::to_string(_port);
    }
    std::string get_ip()
    {
        return _ip;
    }
    uint16_t get_port()
    {
        return _port;
    }
private:
    uint16_t _port;
    std::string _ip;
    struct sockaddr_in _addr;
};

2.3 C++封装TCPServer

在封装TCPServer之前,需要明确一个原则:一个服务器实例只能对应一个业务,不可复制。因此我们设计一个基类禁止拷贝,并定义统一的退出码。这些放在comm.hpp中。

#pragma once
#include 
#include 
#include 
#include 
#include 
#include "InetAddr.hpp"
#include 
#include 
#include 
#include 
#define CONV(x) ((struct sockaddr*)(&x))
#define DEFAULT_BACKLOG 8
#define MAXNUM 1024
enum ExitCode{
    SOCKET = 0,
    BIND,
    LISTEN,
    ACCEPT,
    FORMAT,
    CONNECT,
};
class nocopy{
public:
    nocopy()
    {}
    ~nocopy()
    {}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

server.hpp中,TCPServer期望实现:

  • 初始化和绑定socket套接字
  • 设置监听状态
  • 接受客户端连接请求
  • 连接成功后,通过回调函数执行业务逻辑
#pragma once
#include "com.hpp"
class TcpServer : public nocopy
{
private:
    using func_t = void(*)(int ,InetAddr&, bool&);
public:
    TcpServer(uint16_t port,func_t func)
    :_port(port),_func(func)
    {}
    void init()
    {
        _listensockfd = socket(AF_INET,SOCK_STREAM,0);
        if(_listensockfd < 0)
            exit(ExitCode::SOCKET);
        InetAddr addr(_port);
        int n = bind(_listensockfd,CONV(addr.get_addr()),sizeof(struct sockaddr_in));
        if(n < 0)
            exit(ExitCode::BIND);
        n = listen(_listensockfd,DEFAULT_BACKLOG);
        if(n < 0)
            exit(ExitCode::LISTEN);
    }
    void run()
    {
        _isrunning = true;
        while(_isrunning)
        {
            struct sockaddr_in tmp;
            memset(&tmp,0,sizeof(tmp));
            socklen_t len = sizeof(tmp);
            int fd = accept(_listensockfd,CONV(tmp),&len);
            if(fd < 0)
                exit(ExitCode::ACCEPT);
            InetAddr peer(tmp);
            _func(fd,peer,_isrunning);//回调函数
        }
    }
private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning = false;
    func_t _func;
};

2.4 客户端与服务器构建

客户端需要主动连接服务器,在连接成功之前不能发送数据。

#include "com.hpp"
int main(int argv,char* argc[])
{
    if(argv != 3)
    {
        std::cout<<"format: "<<"./"<<__FILE__<<" ip port"<";
        std::string s;std::cin>>s;
        int n = write(sockfd,s.c_str(),sizeof(s));
        if (n < 0)
            continue;
    }
    return 0;
}

服务器端需要设置好回调函数:

#include "server.hpp"
void func(int fd,InetAddr &addr, bool &isrunning)
{
    while (isrunning)
    {
        char buffer[MAXNUM];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
        if (n < 0)
            continue;
        else if(n == 0)
            break;
        buffer[n] = '\0';
        std::cout<<"client say: "<

运行效果示意:

三、拓展与优化:从单线程到多线程

⚠️ 目前实现的TCPServer是一个单线程/单进程服务器,只能依次处理一个客户端的请求。当多个客户端同时连接时,后续连接会被阻塞。在实际生产环境中,TCPServer通常需要改造成多线程或多进程模型,例如使用线程池、epoll事件驱动或Reactor模式。对于C++开发者,可以结合 std::thread 或 pthread 实现并发处理;而Python的 socketserver 模块和Go的 goroutine 则提供了更上层的抽象。理解底层Socket接口后,再学习这些高级框架会事半功倍。

[AFFILIATE_SLOT_2]

总结

本文系统讲解了Linux下UDP与TCP的Socket编程接口,并通过C++封装展示了服务器与客户端的构建过程。UDP无连接、开销小,适合实时场景;TCP面向连接、可靠传输,适合文件传输和Web服务。掌握这些底层接口后,你可以更从容地应对高并发网络编程挑战。后续可进一步学习多路复用(select/poll/epoll)和多线程服务器模型。

sockaddrbind()connect()accept()
struct sockaddr_in {
    sa_family_t    sin_family;   // AF_INET
    in_port_t      sin_port;     // 端口号(网络字节序)
    struct in_addr sin_addr;     // IPv4 地址(网络字节序)
    char           sin_zero[8];  // 填充,使大小与 sockaddr 一致
};
sockaddrsockaddr_in(struct sockaddr*)