在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*)
浙公网安备 33010602011771号