Linux 网络编程基础:Socket、TCP、UDP 通信原理

发布时间:2026/7/22 18:12:56
Linux 网络编程基础:Socket、TCP、UDP 通信原理 Linux 网络编程基础Socket、TCP、UDP 通信原理一、引言前面的文章都在讲单个设备内部的事情——文件 IO、多进程、多线程、进程间通信。但嵌入式设备不能永远活在孤岛上传感器采集了数据总要通过网络上报给服务器远程控制指令自然也得通过网络下发到设备。这就引出了 Linux 网络编程的核心——Socket套接字。本文从基本概念讲起梳理 TCP 和 UDP 的通信流程差异再给出服务端和客户端的可运行代码。读完这篇你就迈入了联网嵌入式开发的大门。二、什么是 SocketSocket 可以理解为一个网络通信的端点。一台设备有多个网络应用程序操作系统通过 IP 地址 端口号来唯一标识一个 Socket形式为IP:Port。在 Linux 中Socket 也是文件描述符——你可以像读写文件一样read/write一个 Socket。这种统一的设计哲学是 Unix/Linux 最大的优雅之处。Socket 通信的基本流程Server Client | | socket() ─── 创建套接字 socket() ─── 创建套接字 | | bind() ─── 绑定地址端口 | | | listen() ── 开始监听 connect() ── 请求连接 | | accept() ── 接受连接阻塞等待 ──────→ | | | read/write ←─── 数据交换 ────→ read/write | | close() close()三、TCP vs UDP一张表看懂特性TCPUDP连接方式面向连接三次握手无连接可靠性可靠保证数据不丢不乱序不可靠数据可能丢失传输效率较慢有流量控制快直接发包应用场景HTTP、文件传输、远程登录视频流、语音通话、传感器高频采集头部开销20 字节8 字节代码复杂度较复杂有 accept/connect简单一句话总结TCP 像打电话——先拨号接通再通话确保每句话都听清楚UDP 像发短信——直接丢出去对方收到没有你不管。四、TCP 服务端与客户端实战4.1 TCP 服务端#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includearpa/inet.h#definePORT8888#defineBACKLOG5intmain(void){intserver_fd,client_fd;structsockaddr_inserver_addr,client_addr;socklen_taddr_lensizeof(client_addr);charbuf[1024];// 1. 创建 socketserver_fdsocket(AF_INET,SOCK_STREAM,0);if(server_fd0){perror(socket);exit(1);}// 设置端口复用 (避免 Address already in use)intopt1;setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt));// 2. 绑定地址和端口memset(server_addr,0,sizeof(server_addr));server_addr.sin_familyAF_INET;server_addr.sin_addr.s_addrINADDR_ANY;// 监听所有网卡server_addr.sin_porthtons(PORT);// 主机字节序转网络字节序if(bind(server_fd,(structsockaddr*)server_addr,sizeof(server_addr))0){perror(bind);exit(1);}// 3. 监听if(listen(server_fd,BACKLOG)0){perror(listen);exit(1);}printf(TCP 服务端启动,监听端口 %d...\n,PORT);// 4. 接受客户端连接client_fdaccept(server_fd,(structsockaddr*)client_addr,addr_len);if(client_fd0){perror(accept);exit(1);}printf(客户端已连接: %s:%d\n,inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));// 5. 收发数据while(1){memset(buf,0,sizeof(buf));ssize_tnread(client_fd,buf,sizeof(buf)-1);if(n0)break;printf([收到] %s\n,buf);// 回显给客户端charreply[1024];snprintf(reply,sizeof(reply),服务端已收到: %s,buf);write(client_fd,reply,strlen(reply));}printf(客户端断开\n);close(client_fd);close(server_fd);return0;}4.2 TCP 客户端#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includearpa/inet.h#defineSERVER_IP127.0.0.1#definePORT8888intmain(void){intsock_fd;structsockaddr_inserver_addr;charbuf[1024];// 1. 创建 socketsock_fdsocket(AF_INET,SOCK_STREAM,0);// 2. 连接服务器memset(server_addr,0,sizeof(server_addr));server_addr.sin_familyAF_INET;server_addr.sin_porthtons(PORT);inet_pton(AF_INET,SERVER_IP,server_addr.sin_addr);if(connect(sock_fd,(structsockaddr*)server_addr,sizeof(server_addr))0){perror(connect);exit(1);}printf(已连接到服务器\n);// 3. 发送数据constchar*msg温度26.5°C, 湿度65%;write(sock_fd,msg,strlen(msg));// 4. 接收回复memset(buf,0,sizeof(buf));read(sock_fd,buf,sizeof(buf)-1);printf([服务器回复] %s\n,buf);close(sock_fd);return0;}4.3 编译运行# 终端1启动服务端gcc-otcp_server tcp_server.c./tcp_server# 终端2启动客户端gcc-otcp_client tcp_client.c./tcp_client新手常见错误bind: Address already in use上一个服务端没正常退出端口被占用。解决方案加上SO_REUSEADDR选项connect: Connection refused服务端没启动或防火墙拦截了端口字节序搞反IP 地址用inet_pton/inet_ntop端口用htons/ntohs五、UDP 通信实战UDP 不需要listen/accept/connect当然也可以 connect 来固定对端服务端和客户端的区别很模糊。UDP 收发端通用#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includearpa/inet.h#definePORT9999intmain(void){intsock_fd;structsockaddr_inlocal_addr,peer_addr;socklen_taddr_lensizeof(peer_addr);charbuf[1024];sock_fdsocket(AF_INET,SOCK_DGRAM,0);memset(local_addr,0,sizeof(local_addr));local_addr.sin_familyAF_INET;local_addr.sin_addr.s_addrINADDR_ANY;local_addr.sin_porthtons(PORT);bind(sock_fd,(structsockaddr*)local_addr,sizeof(local_addr));printf(UDP 端口 %d 就绪,等待数据...\n,PORT);while(1){memset(buf,0,sizeof(buf));ssize_tnrecvfrom(sock_fd,buf,sizeof(buf)-1,0,(structsockaddr*)peer_addr,addr_len);if(n0){printf([来自 %s:%d] %s\n,inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port),buf);// 回复constchar*replyACK;sendto(sock_fd,reply,strlen(reply),0,(structsockaddr*)peer_addr,addr_len);}}close(sock_fd);return0;}UDP 客户端几乎一样只是把recvfrom换成主动sendtoconstchar*msg传感器心跳: OK;sendto(sock_fd,msg,strlen(msg),0,(structsockaddr*)server_addr,sizeof(server_addr));六、字节序问题必须绕开的坑网络字节序是大端Big Endian而 x86 和 ARM 处理器通常是小端Little Endian。如果直接传原始数据的多字节字段比如uint32_t接收端会得到镜像数据。// 错误示范直接发 uint32_tuint32_tval0x12345678;write(fd,val,sizeof(val));// 小端机器上先发 0x78 0x56 0x34 0x12// 正确做法转换后再发uint32_tnet_valhtonl(val);// → 大端write(fd,net_val,sizeof(net_val));转换函数速查函数含义htonsHost to Network Short16位htonlHost to Network Long32位ntohsNetwork to Host ShortntohlNetwork to Host Long七、总结Socket 是 Linux 网络编程的基础核心记住这几条Socket 也是文件描述符——可读可写可关闭TCP 三步走socket→bind→listen→accept数据用 read/writeUDP 两步走socket→bind数据用 sendto/recvfrom字节序转换htons/htonl送出去ntohs/ntohl接回来端口复用SO_REUSEADDR是服务端标配选项掌握了这套基础后面搞高并发服务端epoll才算有了坚实的底子。