Linux 网络编程基础:Socket、TCP、UDP 通信原理
2026/7/22 18:12:39 网站建设 项目流程

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

一、引言

前面的文章都在讲单个设备内部的事情——文件 IO、多进程、多线程、进程间通信。但嵌入式设备不能永远活在孤岛上:传感器采集了数据,总要通过网络上报给服务器;远程控制指令,自然也得通过网络下发到设备。

这就引出了 Linux 网络编程的核心——Socket(套接字)。本文从基本概念讲起,梳理 TCP 和 UDP 的通信流程差异,再给出服务端和客户端的可运行代码。读完这篇,你就迈入了"联网嵌入式开发"的大门。

二、什么是 Socket?

Socket 可以理解为一个网络通信的端点。一台设备有多个网络应用程序,操作系统通过 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 服务端

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#definePORT8888#defineBACKLOG5intmain(void){intserver_fd,client_fd;structsockaddr_inserver_addr,client_addr;socklen_taddr_len=sizeof(client_addr);charbuf[1024];// 1. 创建 socketserver_fd=socket(AF_INET,SOCK_STREAM,0);if(server_fd<0){perror("socket");exit(1);}// 设置端口复用 (避免 Address already in use)intopt=1;setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));// 2. 绑定地址和端口memset(&server_addr,0,sizeof(server_addr));server_addr.sin_family=AF_INET;server_addr.sin_addr.s_addr=INADDR_ANY;// 监听所有网卡server_addr.sin_port=htons(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_fd=accept(server_fd,(structsockaddr*)&client_addr,&addr_len);if(client_fd<0){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_tn=read(client_fd,buf,sizeof(buf)-1);if(n<=0)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 客户端

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#defineSERVER_IP"127.0.0.1"#definePORT8888intmain(void){intsock_fd;structsockaddr_inserver_addr;charbuf[1024];// 1. 创建 socketsock_fd=socket(AF_INET,SOCK_STREAM,0);// 2. 连接服务器memset(&server_addr,0,sizeof(server_addr));server_addr.sin_family=AF_INET;server_addr.sin_port=htons(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 收发端(通用)

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#definePORT9999intmain(void){intsock_fd;structsockaddr_inlocal_addr,peer_addr;socklen_taddr_len=sizeof(peer_addr);charbuf[1024];sock_fd=socket(AF_INET,SOCK_DGRAM,0);memset(&local_addr,0,sizeof(local_addr));local_addr.sin_family=AF_INET;local_addr.sin_addr.s_addr=INADDR_ANY;local_addr.sin_port=htons(PORT);bind(sock_fd,(structsockaddr*)&local_addr,sizeof(local_addr));printf("UDP 端口 %d 就绪,等待数据...\n",PORT);while(1){memset(buf,0,sizeof(buf));ssize_tn=recvfrom(sock_fd,buf,sizeof(buf)-1,0,(structsockaddr*)&peer_addr,&addr_len);if(n>0){printf("[来自 %s:%d] %s\n",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port),buf);// 回复constchar*reply="ACK";sendto(sock_fd,reply,strlen(reply),0,(structsockaddr*)&peer_addr,addr_len);}}close(sock_fd);return0;}

UDP 客户端几乎一样,只是把recvfrom换成主动sendto

constchar*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_tval=0x12345678;write(fd,&val,sizeof(val));// 小端机器上先发 0x78 0x56 0x34 0x12// 正确做法:转换后再发uint32_tnet_val=htonl(val);// → 大端write(fd,&net_val,sizeof(net_val));

转换函数速查:

函数含义
htonsHost to Network Short(16位)
htonlHost to Network Long(32位)
ntohsNetwork to Host Short
ntohlNetwork to Host Long

七、总结

Socket 是 Linux 网络编程的基础,核心记住这几条:

  1. Socket 也是文件描述符——可读可写可关闭
  2. TCP 三步走:socket→bind→listen→accept,数据用 read/write
  3. UDP 两步走:socket→bind,数据用 sendto/recvfrom
  4. 字节序转换htons/htonl送出去,ntohs/ntohl接回来
  5. 端口复用SO_REUSEADDR是服务端标配选项

掌握了这套基础,后面搞高并发服务端(epoll)才算有了坚实的底子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询