Linux 网络学习UDP 与 TCP 套接字引言在 Linux 网络编程里套接字socket是应用程序与内核网络协议栈之间的接口。程序拿到一个套接字描述符就可以像操作文件一样读写网络数据。真正让初学者容易混淆的地方有两个UDP 的数据报到底怎样到达对端TCP 为什么需要listen和accept以及这两个协议为什么不能只靠一个“发送函数”就解决所有问题。下面分两篇整理 UDP 和 TCP从回显程序讲到聊天室、线程池和应用层协议。第一篇UDP 套接字1. UDP 的通信模型UDPUser Datagram Protocol用户数据报协议把一次发送看成一个独立的数据报。发送方给出数据、目标 IP 和目标端口内核把这三个信息交给 UDP 层接收方通过recvfrom一次取出一个数据报同时得到发送方的地址。它不建立连接也不保证数据一定到达、按顺序到达或只到达一次。这个特性让 UDP 的接口很直接服务器创建一个数据报套接字绑定一个固定端口然后等待数据客户端知道服务器的 IP 和端口后直接调用sendto。可以把 UDP 想成寄快递。每个包裹上都写着收件地址快递员把包裹送到目的地寄件人不需要先和收件人建立一条专线。包裹之间也没有天然的连续关系应用程序要不要重传、编号、去重都由自己决定。2. 套接字、地址和字节序创建 IPv4 UDP 套接字的核心代码是intsockfdsocket(AF_INET,SOCK_DGRAM,0);AF_INET表示 IPv4 地址族SOCK_DGRAM表示数据报语义。返回值是文件描述符小于 0 说明创建失败。IPv4 地址通常放在sockaddr_in中structsockaddr_inaddr{};addr.sin_familyAF_INET;addr.sin_porthtons(port);addr.sin_addr.s_addrinet_addr(0.0.0.0);端口和地址在网络上传输时采用网络字节序通常是大端主机内部的整数顺序可能不同所以要使用htonshost to network short把主机端口转成网络端口使用ntohs把收到的端口转回主机端口。IP 字符串和四字节地址之间则可以用inet_addr和inet_ntoa完成转换。0.0.0.0与INADDR_ANY表示监听本机的任意 IPv4 网卡。服务器一般这样绑定局域网地址、回环地址和其他网卡地址都可以把数据交给同一个端口。3. UDP 服务器的固定流程UDP 服务器的流程可以压缩成四步socket创建套接字。填充本地端口和地址并用bind交给内核。recvfrom接收数据和对端地址。sendto根据对端地址回复数据。bind很关键。服务器端口必须固定客户端才能找到它。下面是回显服务器的处理循环intsockfdsocket(AF_INET,SOCK_DGRAM,0);sockaddr_in local{};local.sin_familyAF_INET;local.sin_porthtons(8888);local.sin_addr.s_addrINADDR_ANY;if(bind(sockfd,reinterpret_castsockaddr*(local),sizeof(local))0){perror(bind);return1;}while(true){charbuffer[1024]{};sockaddr_in peer{};socklen_t lensizeof(peer);ssize_t nrecvfrom(sockfd,buffer,sizeof(buffer)-1,0,reinterpret_castsockaddr*(peer),len);if(n0){continue;}buffer[n]\0;std::string replyserver echo# ;replybuffer;sendto(sockfd,reply.c_str(),reply.size(),0,reinterpret_castsockaddr*(peer),len);}这里没有listen也没有accept。UDP 内核只关心“这个数据报应该交给哪个本地端口”服务器收到数据后可以直接回复。4. 客户端为什么通常不手动 bindUDP 客户端需要知道服务器地址却通常不需要指定自己的端口。第一次调用sendto时如果套接字还没有本地地址内核会选择一个临时端口并完成隐式绑定。这个端口只要在当前通信中唯一即可客户端退出后通常可以再次变化。客户端的关键片段如下intsockfdsocket(AF_INET,SOCK_DGRAM,0);sockaddr_in server{};server.sin_familyAF_INET;server.sin_porthtons(server_port);server.sin_addr.s_addrinet_addr(server_ip.c_str());sendto(sockfd,message.c_str(),message.size(),0,reinterpret_castsockaddr*(server),sizeof(server));charbuffer[1024]{};sockaddr_in peer{};socklen_t lensizeof(peer);ssize_t nrecvfrom(sockfd,buffer,sizeof(buffer)-1,0,reinterpret_castsockaddr*(peer),len);如果客户端手动绑定固定端口多个客户端就可能因为端口冲突而启动失败。只有当业务明确要求固定源端口例如某些服务发现或协议约定才需要显式bind。5. UDP 聊天室一个套接字服务多个用户聊天室进一步说明了 UDP 的特点。服务器只需要一个 UDP 套接字却可以服务多个客户端因为每个数据报都自带发送方地址。服务器维护一个用户地址列表收到新地址时加入列表然后遍历列表调用sendto广播。核心逻辑大致如下voidBroadcast(intsockfd,conststd::stringmessage){for(constInetAddruser:users){sendto(sockfd,message.c_str(),message.size(),0,reinterpret_castconstsockaddr*(user.GetNetAddress()),user.Len());}}代码中的用户身份实际上是IP port。同一台机器上启动两个客户端它们通常会拿到不同的临时端口因此可以被服务器区分。服务器还需要避免重复加入同一个地址并在用户离线时删除地址。客户端把接收和发送拆成两个线程服务器把广播任务放进线程池让接收循环尽快返回。6. UDP 编程中容易忽略的地方第一UDP 保留数据报边界。一次sendto对应一个数据报接收端一次recvfrom取一个数据报如果缓冲区太小超出的部分可能被截断。因此消息大小和缓冲区上限要有明确约定。第二recvfrom返回的地址是本次数据报的来源地址不能把上一次收到的地址当成当前用户。聊天室广播时要使用每个用户自己的sockaddr_in。第三返回值必须检查。sendto或recvfrom小于 0 都代表系统调用失败。第四UDP 没有连接关闭事件。在线状态需要心跳、超时或业务消息来维护如果业务要求可靠性还要自己设计序号、确认和重传。第二篇TCP 套接字1. TCP 的通信模型TCPTransmission Control Protocol传输控制协议面向连接提供可靠的字节流。通信双方先建立连接再通过连接读写数据TCP 会负责确认、重传、排序和流量控制。TCP 更像打电话。双方先拨号并接通接通后可以连续说话“说了几句话”并不是网络层天然知道的边界应用程序需要自己规定一条消息从哪里开始、到哪里结束。TCP 服务器有两个不同角色的套接字监听套接字负责接收连接连接套接字负责和某一个客户端通信。listen之后的套接字不能直接拿来读业务数据必须通过accept得到新的服务套接字。2. TCP 服务器的五步服务器初始化通常是socket(AF_INET, SOCK_STREAM, 0)创建流式套接字。bind固定本地 IP 和端口。listen把套接字变成监听状态并设置等待队列长度。accept取出一个已经完成连接的新套接字。对新套接字read/recv和write/send通信结束后close。对应的初始化代码如下intlistensocksocket(AF_INET,SOCK_STREAM,0);sockaddr_in local{};local.sin_familyAF_INET;local.sin_porthtons(8888);local.sin_addr.s_addrINADDR_ANY;bind(listensock,reinterpret_castsockaddr*(local),sizeof(local));listen(listensock,16);接收连接的循环while(true){sockaddr_in client{};socklen_t lensizeof(client);intsockfdaccept(listensock,reinterpret_castsockaddr*(client),len);if(sockfd0){continue;}service(sockfd,client);close(sockfd);}listen的第二个参数是等待队列的参考长度不是“最多只能连接多少客户端”。真正能服务多少连接还取决于进程数、线程数、文件描述符上限、内存和业务处理速度。3. 为什么 accept 会返回新的文件描述符监听套接字可以理解为前台接待员只负责接收新客户accept返回的服务套接字才是与某一位客户的专线。每来一个客户端就多一个服务套接字但监听套接字继续留在accept循环中。这也是 TCP 与 UDP 服务器结构差异最大的地方UDP 一个套接字可以收到所有客户端的数据报TCP 则通过多个服务套接字区分不同连接。客户端地址可以通过accept的sockaddr_in得到再用ntohs和inet_ntoa转成可读的 IP 与端口。4. TCP 客户端的 connectTCP 客户端创建SOCK_STREAM套接字后使用connect发起连接intsockfdsocket(AF_INET,SOCK_STREAM,0);sockaddr_in serverMakeAddress(server_ip,server_port);if(connect(sockfd,reinterpret_castsockaddr*(server),sizeof(server))0){perror(connect);return1;}客户端一般不显式绑定本地端口。connect前后内核会为客户端选择合适的本地地址和临时端口形成四元组本地 IP、本地端口、服务器 IP、服务器端口。服务器上的每条 TCP 连接都由这个四元组唯一确定。建立连接后双方可以使用read/write或recv/send。练习中的回显客户端从标准输入读取一行调用write发给服务器再调用read读取回显结果。5. read 返回值代表什么对 TCP 服务套接字调用read时返回值要分三种情况处理大于 0本次读到了若干字节。等于 0对端已经有序关闭连接通常称为读到文件结束。小于 0发生错误具体原因需要结合errno判断。read一次读到的字节数不等于对端一次write的字节数。一次写入可能被分成多次读出多次写入也可能合并到一次读取中这就是常说的粘包和拆包现象。TCP 只保证字节顺序不知道你的业务消息边界。6. 从短连接到长连接最早的回显服务器在处理完一次read和write后就关闭服务套接字这属于短连接。它结构简单但每次请求都要重新建立连接。把read/write/close放进循环就能得到长连接while(true){charbuffer[1024]{};ssize_t nread(sockfd,buffer,sizeof(buffer)-1);if(n0){break;}buffer[n]\\0;write(sockfd,buffer,n);}close(sockfd);长连接适合交互式命令、聊天室、远程终端等场景但服务端必须保存连接状态并考虑客户端长时间不发数据时如何释放资源。7. 一个连接交给谁处理练习代码展示了多进程、多线程和线程池三种模型。多进程中父进程负责accept子进程负责业务多线程中每条连接交给一个线程线程池则由主线程接入连接再把任务放进队列。线程池能限制线程数量把接入和业务解耦但仍要正确管理文件描述符和共享数据。连接数继续增大时可以用select、poll或epoll观察多个文件描述符。它们解决事件等待问题不会替代 TCP 连接语义也不会自动定义消息协议。8. 应用层协议给字节流划分消息TCP 编程最容易留下的坑是只写一个read然后假设读到的就是完整请求。正确的做法是定义应用层协议。练习中的计算器协议采用“长度 分隔符 JSON 分隔符”的格式长度\\r\\nJSON内容\\r\\n例如一条请求可以表示为31\\r\\n{left:10,right:20,oper:}\\r\\n接收端维护一个字符串缓冲区。每次recv把新字节追加到缓冲区然后按下面的顺序处理查找第一个\\r\\n得到长度字段。判断缓冲区是否已经包含完整的 JSON 和结尾分隔符。如果不完整保留缓冲区等待下一次接收。如果完整提取一条消息删除已经消费的字节。循环处理缓冲区中剩余的完整消息。这种做法既能处理半包也能处理一次读到多条消息。JSON 只是序列化格式长度字段才负责告诉接收端消息边界。真实项目还要检查长度上限、非法字符和数字转换异常避免恶意数据让内存或 CPU 无限消耗。9. TCP 编程中容易忽略的地方服务重启时可在bind前设置SO_REUSEADDR。write和send也可能只发送一部分数据大消息要循环发送非阻塞套接字还要处理EAGAIN。shutdown可以只关闭读或写的一半close才释放文件描述符。线程池任务不要引用已经离开作用域的地址和字符串。TCP 只保证字节可靠到达协议错误和业务失败仍要由应用处理。UDP 与 TCP 的选择对比项UDPTCP通信方式无连接、数据报面向连接、字节流数据边界保留一次发送的边界没有消息边界需要应用层分帧可靠性不保证到达、顺序和唯一性内置确认、重传和排序服务端核心调用socket、bind、recvfrom、sendtosocket、bind、listen、accept、recv/send客户端常见行为第一次发送时由内核自动分配源端口connect时建立连接并分配源端口适合场景DNS、实时状态、广播、简单发现HTTP、远程登录、文件传输、可靠请求选择协议时先看数据丢失能否接受以及消息边界和连接状态是否明确。实时场景可选 UDP可靠请求选 TCPUDP 的可靠性要自带序号、确认和重传。当这些问题都能解释清楚时套接字就不再只是几组函数调用UDP 的核心是“带地址的数据报”TCP 的核心是“连接上的有序字节流”而服务器的并发模型和应用层协议则决定了这两种传输能力能否变成可靠的程序。