上一节我们学习了网络分层模型TCP/IP 五层模型明确了应用层与传输层的核心作用 —— 应用层负责提供具体网络服务传输层TCP/UDP负责端到端的可靠传输。本节课我们将基于网络分层的基础正式进入 Linux 网络编程的核心 ——Socket 编程理解 Socket 的本质、核心概念和基本操作最终实现一个简单的 TCP 客户端和服务器程序完成首次网络通信为后续复杂网络程序开发打下基础。核心重点掌握 Socket 的定义与作用、TCP 通信的核心流程、Linux 中 Socket 相关系统调用能够独立编写简单的 TCP 客户端和服务器完成数据的发送与接收。一、Socket 核心概念必懂在 Linux 网络编程中Socket套接字是应用层与传输层之间的接口是网络通信的 “端点”—— 简单说Socket 就是一个 “通信通道”应用程序通过这个通道向传输层发送数据、接收传输层传来的数据无需关心底层网络层、数据链路层、物理层的传输细节这些由 Linux 内核的 TCP/IP 协议栈自动处理。类比理解Socket 就像我们打电话时的 “听筒 话筒”我们应用程序通过听筒听、话筒说无需关心电话线路底层传输如何传递声音信号只需通过这个 “接口” 完成通话。1. Socket 的本质Socket 在 Linux 中本质是一个文件描述符和我们之前学习的文件、管道的文件描述符一致Linux 系统遵循 “一切皆文件” 的理念因此 Socket 的操作创建、读写、关闭和文件操作open、read、write、close的逻辑完全一致极大降低了编程复杂度。核心特点一个 Socket 由 “IP 地址 端口号” 唯一标识结合传输层协议TCP/UDP可唯一确定网络中的一个应用程序通信端点。例如192.168.1.100:8888TCP就代表 192.168.1.100 这台主机上端口为 8888 的 TCP 应用程序的通信端点。2. Socket 的类型重点掌握 2 种Linux 中常用的 Socket 类型有两种对应不同的传输层协议我们本节课重点学习流式套接字对应 TCPSOCK_STREAM流式套接字对应 TCP 协议面向连接、可靠传输、面向字节流数据传输有序、无丢失、无重复适用于文件传输、远程登录等场景本节课重点实现SOCK_DGRAM数据报套接字对应 UDP 协议无连接、不可靠传输、面向数据报数据传输速度快可能出现丢失、乱序适用于视频直播、游戏等实时性要求高的场景后续章节讲解。3. Socket 编程的核心流程TCPTCP 是面向连接的协议因此 TCP Socket 编程的核心是 “建立连接→通信→关闭连接”客户端和服务器的流程不同需重点区分记牢这个流程编程时直接套用服务器端流程被动接收连接创建 Socketsocket ()创建一个通信端点获取 Socket 文件描述符绑定地址bind ()将 Socket 与 “IP 地址 端口号” 绑定明确服务器的通信地址监听连接listen ()将 Socket 设置为监听状态等待客户端发起连接接受连接accept ()阻塞等待客户端连接连接成功后返回一个新的 Socket 文件描述符用于与该客户端通信读写数据read ()/write ()通过新的 Socket与客户端进行数据交互关闭 Socketclose ()通信结束后关闭 Socket释放资源。客户端流程主动发起连接创建 Socketsocket ()创建通信端点获取 Socket 文件描述符发起连接connect ()向服务器的 “IP 地址 端口号” 发起连接请求读写数据read ()/write ()连接成功后与服务器进行数据交互关闭 Socketclose ()通信结束后关闭 Socket释放资源。关键区别服务器端需要 “绑定、监听、接受连接” 三个额外步骤客户端直接发起连接即可服务器端会有两个 Socket监听 Socket 和通信 Socket客户端只有一个通信 Socket。二、Linux Socket 核心系统调用实操重点编写 TCP Socket 程序核心是调用 Linux 系统提供的 Socket 相关函数以下是本节课必备的 5 个系统调用详细讲解参数含义和使用场景结合代码示例理解无需死记硬背重点掌握用法。1. 创建 Socketsocket ()函数原型sys/socket.hint socket(int domain, int type, int protocol);#### 参数说明 - domain协议族地址族指定Socket的通信范围Linux中常用AF_INETIPv4协议最常用 - typeSocket类型本节课用SOCK_STREAM流式套接字TCP - protocol协议类型一般设为0表示根据前两个参数自动选择对应协议TCP对应IPPROTO_TCPUDP对应IPPROTO_UDP。 #### 返回值 成功返回一个非负整数Socket文件描述符失败返回-1设置errno错误码。 #### 示例创建TCP Socket c // 创建TCP SocketIPv4协议 int sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd -1) { perror(socket create failed); // 打印错误信息 exit(1); // 退出程序 }2. 绑定地址bind ()函数原型sys/socket.h int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明sockfdsocket () 函数返回的 Socket 文件描述符addr指向 socket 地址结构的指针存储要绑定的 “IP 地址 端口号”IPv4 对应 struct sockaddr_in需强制转换为 struct sockaddr *addrlensocket 地址结构的长度用 sizeof (struct sockaddr_in) 获取。关键结构struct sockaddr_inIPv4 地址结构netinet/in.hstruct sockaddr_in {sa_family_t sin_family; // 协议族必须设为 AF_INETIPv4uint16_t sin_port; // 端口号需转换为网络字节序htons () 函数struct in_addr sin_addr; // IP 地址需转换为网络字节序inet_addr () 函数unsigned char sin_zero [8];// 填充字段设为 0 即可}; // IP 地址结构sin_addr 的类型struct in_addr {in_addr_t s_addr; // 32 位 IPv4 地址网络字节序};#### 重要注意字节序转换 计算机存储数据的字节序有两种主机字节序小端Linux主机默认和网络字节序大端网络通信标准因此端口号和IP地址必须转换为网络字节序否则会出现通信失败。 - 端口号转换htons(port)host to network short主机字节序→网络字节序适用于16位端口号 - IP地址转换inet_addr(ip)将字符串格式的IP地址转换为网络字节序的32位整数如192.168.1.100→网络字节序 - 特殊IPINADDR_ANY表示绑定本机所有可用IP地址无需手动指定具体IP适合服务器。 #### 示例绑定IP和端口 c struct sockaddr_in server_addr; // 初始化地址结构 memset(server_addr, 0, sizeof(server_addr)); // 清空结构 server_addr.sin_family AF_INET; // IPv4协议 server_addr.sin_port htons(8888); // 端口号8888转换为网络字节序 server_addr.sin_addr.s_addr INADDR_ANY; // 绑定本机所有IP // 绑定Socket和地址 int ret bind(sockfd, (struct sockaddr*)server_addr, sizeof(server_addr)); if (ret -1) { perror(bind failed); close(sockfd); // 关闭Socket释放资源 exit(1); }3. 监听连接listen ()函数原型int listen(int sockfd, int backlog);参数说明sockfd绑定后的 Socket 文件描述符backlog监听队列的最大长度即等待连接的客户端最大数量一般设为 5、10 即可具体根据需求调整。返回值成功返回 0失败返回 - 1设置 errno。示例监听连接int ret listen(sockfd, 10); // 监听队列最大长度10 if (ret -1) { perror(listen failed); close(sockfd); exit(1); } printf(服务器已启动监听端口8888...\n);4. 接受连接accept ()函数原型int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数说明sockfd监听状态的 Socket 文件描述符addr指向客户端地址结构的指针用于存储发起连接的客户端的 “IP 地址 端口号”可设为 NULL表示不关心客户端信息addrlen指向客户端地址结构长度的指针需先初始化设为 sizeof (struct sockaddr_in)。关键特性accept () 函数是阻塞函数 —— 如果没有客户端发起连接程序会一直停在这个函数直到有客户端连接成功。返回值成功返回一个新的非负整数通信 Socket 文件描述符用于与当前客户端通信失败返回 - 1设置 errno。示例接受客户端连接struct sockaddr_in client_addr; socklen_t client_addr_len sizeof(client_addr); // 阻塞等待客户端连接 int connfd accept(sockfd, (struct sockaddr*)client_addr, client_addr_len); if (connfd -1) { perror(accept failed); close(sockfd); exit(1); } // 打印客户端信息inet_ntoa()将网络字节序IP转为字符串 printf(客户端连接成功IP%s端口%d\n, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // ntohs()网络字节序→主机字节序5. 发起连接connect ()函数原型客户端专用sys/socket.h int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明sockfd客户端 Socket 文件描述符addr指向服务器地址结构的指针存储服务器的 “IP 地址 端口号”addrlen服务器地址结构的长度。关键特性connect () 函数也是阻塞函数 —— 如果服务器未响应程序会一直阻塞直到连接成功或超时。返回值成功返回 0失败返回 - 1设置 errno如连接被拒绝、服务器不可达。示例客户端发起连接struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(8888); // 服务器端口号 server_addr.sin_addr.s_addr inet_addr(192.168.1.100); // 服务器IP地址 // 发起连接 int ret connect(sockfd, (struct sockaddr*)server_addr, sizeof(server_addr)); if (ret -1) { perror(connect failed); close(sockfd); exit(1); } printf(连接服务器成功\n);6. 读写数据与关闭 Socket读写数据连接成功后客户端和服务器通过 read () 和 write () 函数读写数据用法和文件读写完全一致c#include unistd.h // 读数据从Socket读取数据到缓冲区 ssize_t read(int fd, void *buf, size_t count); // 写数据将缓冲区的数据写入Socket ssize_t write(int fd, const void *buf, size_t count);参数说明fd 为通信 Socket 文件描述符服务器用 accept () 返回的 connfd客户端用 socket () 返回的 sockfdbuf 为数据缓冲区count 为要读写的字节数。返回值成功返回实际读写的字节数失败返回 - 1read () 返回 0表示对方关闭连接。关闭 Socket通信结束后用 close () 函数关闭 Socket释放文件描述符和网络资源int close(int fd);注意服务器端需关闭两个 Socket监听 sockfd 和通信 connfd客户端只需关闭一个 Socketsockfd。三、完整实现TCP 客户端与服务器程序结合上述系统调用我们实现一个简单的 TCP 客户端和服务器服务器监听 8888 端口客户端连接服务器后发送一条消息如 “Hello, Linux Socket!”服务器接收消息后回复一条确认消息如 “收到消息XXX”然后双方关闭连接。环境说明Linux 系统CentOS/Ubuntu 均可、GCC 编译器客户端和服务器可在同一台主机IP 设为 127.0.0.1或不同主机需确保网络连通运行。1. TCP 服务器程序server.csys/socket.h #includenetinet/in.hunistd.hstdio.hstdlib.hstring.h int main () { // 1. 创建 TCP Socket int sockfd socket (AF_INET, SOCK_STREAM, 0); if (sockfd -1) { perror (socket create failed); exit (1); } // 2. 绑定 IP 和端口 struct sockaddr_in server_addr; memset (server_addr, 0, sizeof (server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons (8888); server_addr.sin_addr.s_addr INADDR_ANY; // 绑定本机所有 IP int ret bind(sockfd, (struct sockaddr*)server_addr, sizeof(server_addr)); if (ret -1) { perror(bind failed); close(sockfd); exit(1); } // 3. 监听连接 ret listen (sockfd, 10); if (ret -1) { perror (listen failed); close (sockfd); exit (1); } printf (服务器已启动监听端口 8888...\n); // 4. 接受客户端连接阻塞等待 struct sockaddr_in client_addr; socklen_t client_addr_len sizeof (client_addr); int connfd accept (sockfd, (struct sockaddr*)client_addr, client_addr_len); if (connfd -1) { perror (accept failed); close (sockfd); exit (1); } printf (客户端连接成功IP% s端口% d\n, inet_ntoa (client_addr.sin_addr), ntohs (client_addr.sin_port)); // 5. 读写数据 char buf [1024] {0}; // 数据缓冲区 // 读取客户端发送的消息 ssize_t read_len read (connfd, buf, sizeof (buf) - 1); // 留 1 个字节存 \0 if (read_len -1) { perror (read failed); close (connfd); close (sockfd); exit (1); } else if (read_len 0) { printf (客户端已关闭连接 \n); close (connfd); close (sockfd); return 0; } // 打印客户端消息 printf (收到客户端消息% s\n, buf); // 回复客户端消息 char reply [1024] {0}; sprintf (reply, 收到消息% s, buf); ssize_t write_len write (connfd, reply, strlen (reply)); if (write_len -1) { perror (write failed); close (connfd); close (sockfd); exit (1); } printf (已回复客户端消息 \n); // 6. 关闭 Socket close (connfd); // 关闭与客户端的通信 Socket close (sockfd); // 关闭监听 Socket printf (服务器已关闭连接 \n); return 0; }### 2. TCP客户端程序client.c c #include sys/socket.h netinet/in.hunistdstdiostdlibstring.h int main() { // 1. 创建TCP Socket int sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd -1) { perror(socket create failed); exit(1); } // 2. 发起连接连接服务器 struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(8888); // 服务器端口号 // 服务器IP地址同一台主机用127.0.0.1不同主机填服务器实际IP server_addr.sin_addr.s_addr inet_addr(127.0.0.1); int ret connect(sockfd, (struct sockaddr*)server_addr, sizeof(server_addr)); if (ret -1) { perror(connect failed); close(sockfd); exit(1); } printf(连接服务器成功\n); // 3. 读写数据 char msg[] Hello, Linux Socket!; // 发送消息给服务器 ssize_t write_len write(sockfd, msg, strlen(msg)); if (write_len -1) { perror(write failed); close(sockfd); exit(1); } printf(已发送消息给服务器%s\n, msg); // 读取服务器的回复 char buf[1024] {0}; ssize_t read_len read(sockfd, buf, sizeof(buf) - 1); if (read_len -1) { perror(read failed); close(sockfd); exit(1); } else if (read_len 0) { printf(服务器已关闭连接\n); close(sockfd); return 0; } printf(收到服务器回复%s\n, buf); // 4. 关闭Socket close(sockfd); printf(客户端已关闭连接\n); return 0; }四、编译与运行实操步骤1. 编译程序在 Linux 终端中进入代码所在目录执行以下命令编译服务器和客户端程序GCC 编译器# 编译服务器程序生成可执行文件server gcc server.c -o server # 编译客户端程序生成可执行文件client gcc client.c -o client编译成功后目录下会生成两个可执行文件server服务器和 client客户端。2. 运行程序注意顺序排查与解决3. 读写数据失败read/write failed原因排查与解决六、学习小结本节课实现的是 “单客户端” 的 TCP 服务器只能处理一个客户端的连接下一节我们将优化服务器实现多客户端并发处理使用多线程或 IO 复用让服务器能够同时响应多个客户端的请求。先启动服务器./server服务器启动后会打印 “服务器已启动监听端口 8888...”进入阻塞状态等待客户端连接。再启动客户端打开新的终端./client3. 预期运行结果服务器终端输出服务器已启动监听端口8888... 客户端连接成功IP127.0.0.1端口12345端口随机 收到客户端消息Hello, Linux Socket! 已回复客户端消息 服务器已关闭连接客户端终端输出连接服务器成功 已发送消息给服务器Hello, Linux Socket! 收到服务器回复收到消息Hello, Linux Socket! 客户端已关闭连接五、常见问题与排查重点编写和运行 Socket 程序时容易出现以下问题结合网络分层知识和 Linux 命令排查重点掌握排查思路1. 绑定失败bind failed: Address already in use原因端口号已被其他程序占用如之前运行的服务器未正常关闭端口未释放。排查与解决# 查看8888端口的占用情况 lsof -i:8888 # 或 ss -tuln | grep 8888 # 杀死占用端口的进程pid为进程ID从上述命令中获取 kill -9 pid # 或修改服务器端口号如改为8889重新编译运行2. 连接失败connect failed: Connection refused原因服务器未启动服务器 IP 地址或端口号填写错误服务器和客户端网络不通如防火墙阻挡。确认服务器已启动且监听的端口号正确用 ping 命令测试服务器和客户端的网络连通性如 ping 127.0.0.1关闭 Linux 防火墙临时测试用systemctl stop firewalld。对方已关闭 Socketread 返回 0Socket 文件描述符错误如未创建成功、已关闭检查 Socket 创建、绑定、连接是否成功确保读写时使用的是正确的 Socket 文件描述符服务器用 connfd客户端用 sockfd增大缓冲区大小如将 buf 设为 2048 字节。Socket 是应用层与传输层的接口本质是 Linux 文件描述符操作逻辑与文件一致TCP Socket 编程的核心流程服务器创建→绑定→监听→接受→读写→关闭客户端创建→连接→读写→关闭重点掌握 5 个核心系统调用socket ()、bind ()、listen ()、accept ()、connect ()以及 read ()、write ()、close () 的使用字节序转换htons ()、inet_addr ()是关键必须将端口和 IP 转换为网络字节序否则会通信失败掌握常见问题的排查方法结合 Linux 命令lsof、ss、ping定位故障培养实操能力。缓冲区溢出读写数据超过缓冲区大小。