news 2026/5/4 17:46:33

Linux TCP 网络编程完全指南:从三次握手到高并发服务器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux TCP 网络编程完全指南:从三次握手到高并发服务器

引言

网络编程是Linux系统编程的重要组成部分。在前面的课程中,我们学习了多线程、多进程和进程间通信。今天,我们将进入网络编程的世界,学习如何使用TCP协议实现客户端和服务器端的通信。

网络编程的核心是套接字,它提供了一种跨主机通信的机制。与之前学习的管道、共享内存等IPC机制不同,网络编程允许不同计算机上的进程进行通信。

本文将涵盖:

  • TCP协议的核心概念(三次握手、四次挥手)

  • TCP服务器和客户端的完整实现

  • 高并发服务器的实现(多进程和多线程版本)

  • 僵尸进程的处理


第一部分:TCP协议基础

一、TCP协议的特点

特性说明
面向连接通信前必须建立连接(三次握手)
可靠传输确认重传机制,保证数据不丢失
面向字节流数据没有边界,像流水一样传输
全双工通信双方可同时发送和接收数据

二、TCP头部结构

TCP头部共20字节固定部分,加上最多40字节的选项字段。

关键标志位:

标志位含义
SYN同步序列号,用于建立连接
ACK确认号有效
FIN发送方已无数据,请求关闭连接
RST重置连接
PSH立即推送数据
URG紧急指针有效

三、TCP三次握手

三次握手是TCP建立连接的过程,目的是让双方确认彼此的发送和接收能力正常。

客户端 服务器 │ │ │─────────── SYN=1, seq=I ───────────────────→ │ 第一次握手 │ │ │←──── SYN=1, ACK=1, seq=J, ack=I+1 ────────── │ 第二次握手 │ │ │─────────── ACK=1, seq=I+1, ack=J+1 ────────→ │ 第三次握手 │ │

三次握手详解:

步骤方向报文内容说明
1客户端 → 服务器SYN=1, seq=I客户端请求建立连接
2服务器 → 客户端SYN=1, ACK=1, seq=J, ack=I+1服务器确认并回应
3客户端 → 服务器ACK=1, seq=I+1, ack=J+1客户端确认,连接建立

四、TCP四次挥手

四次挥手是TCP关闭连接的过程。

客户端 服务器 │ │ │─────────── FIN=1, seq=N ──────────────────→ │ 第一次挥手 │ │ │←─────────── ACK=1, ack=N+1 ──────────────── │ 第二次挥手 │ │ │←─────────── FIN=1, seq=M ────────────────── │ 第三次挥手 │ │ │─────────── ACK=1, ack=M+1 ────────────────→ │ 第四次挥手 │ │

四次挥手详解:

步骤方向报文内容说明
1主动方 → 被动方FIN=1, seq=N主动方请求关闭连接
2被动方 → 主动方ACK=1, ack=N+1被动方确认收到
3被动方 → 主动方FIN=1, seq=M被动方也请求关闭
4主动方 → 被动方ACK=1, ack=M+1主动方确认,连接关闭

为什么是四次而不是三次?
因为TCP是全双工的,双方都需要独立关闭自己的发送通道。被动方收到FIN后,可能还有数据要发送,所以先回复ACK,等数据发送完后再发送FIN。


第二部分:TCP编程核心接口

一、套接字创建——socket()

#include <sys/socket.h> int socket(int domain, int type, int protocol);
参数常用值说明
domainAF_INETIPv4地址族
AF_INET6IPv6地址族
typeSOCK_STREAMTCP流式服务
SOCK_DGRAMUDP数据报服务
protocol0默认协议

返回值:成功返回套接字描述符(类似文件描述符),失败返回-1。

二、绑定地址——bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

用于将套接字与指定的IP地址和端口号绑定。

struct sockaddr_in { sa_family_t sin_family; // 地址族,AF_INET in_port_t sin_port; // 端口号(网络字节序) struct in_addr sin_addr; // IP地址 }; struct in_addr { uint32_t s_addr; // IP地址(网络字节序) };

三、监听连接——listen()

int listen(int sockfd, int backlog);

backlog参数指定已完成握手队列的最大长度。

注意:

  • Linux系统中,backlog表示已完成握手队列的长度

  • 某些Unix系统中,backlog表示未完成+已完成握手队列的总长度

  • 现代内核中队列长度已大幅增加

四、接受连接——accept()

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

从已完成握手队列中取出一个连接,返回一个新的套接字描述符,用于与客户端通信。

阻塞特性:如果队列为空,accept()会阻塞等待。

五、连接服务器——connect()

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端使用此函数向服务器发起连接。

六、收发数据——recv()/send()

ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);
函数方向说明
recv()接收数据从套接字读取数据
send()发送数据向套接字写入数据

flags参数通常设为0,表示无特殊选项。

七、字节序转换函数

网络中统一使用大端字节序(网络字节序),需要进行转换:

#include <arpa/inet.h> // 主机字节序 → 网络字节序 uint16_t htons(uint16_t hostshort); // 短整型(端口) uint32_t htonl(uint32_t hostlong); // 长整型(IP地址) // 网络字节序 → 主机字节序 uint16_t ntohs(uint16_t netshort); uint32_t ntohl(uint32_t netlong); // IP地址转换 in_addr_t inet_addr(const char *cp); // 字符串 → 整数 char *inet_ntoa(struct in_addr in); // 整数 → 字符串

第三部分:TCP服务器实现

一、基础版本(单线程)

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 6000 #define BUFFER_SIZE 128 int main() { int listen_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; char buffer[BUFFER_SIZE]; // 1. 创建套接字 listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == -1) { perror("socket error"); exit(1); } // 2. 绑定地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind error"); exit(1); } // 3. 监听 if (listen(listen_fd, 5) == -1) { perror("listen error"); exit(1); } printf("服务器启动成功,端口:%d\n", PORT); while (1) { // 4. 接受连接 client_len = sizeof(client_addr); client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd == -1) { perror("accept error"); continue; } printf("客户端连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 5. 接收数据 memset(buffer, 0, BUFFER_SIZE); int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (n > 0) { printf("收到数据:%s\n", buffer); send(client_fd, "OK", 2, 0); } // 6. 关闭连接 close(client_fd); } close(listen_fd); return 0; }

二、多进程版本(解决并发问题)

单线程版本的recv()会阻塞,导致无法同时处理多个客户端。使用多进程可以解决这个问题:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <sys/socket.h> #include <sys/wait.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 6000 #define BUFFER_SIZE 128 void handle_client(int client_fd, struct sockaddr_in client_addr) { char buffer[BUFFER_SIZE]; printf("客户端连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); while (1) { memset(buffer, 0, BUFFER_SIZE); int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (n == 0) { printf("客户端已断开:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; } if (n == -1) { perror("recv error"); break; } printf("[%s:%d] %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer); send(client_fd, "OK", 2, 0); } close(client_fd); exit(0); } int main() { int listen_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; // 忽略SIGCHLD信号,避免僵尸进程 signal(SIGCHLD, SIG_IGN); // 创建套接字 listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == -1) { perror("socket error"); exit(1); } // 绑定地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind error"); exit(1); } // 监听 if (listen(listen_fd, 5) == -1) { perror("listen error"); exit(1); } printf("多进程服务器启动成功,端口:%d\n", PORT); while (1) { client_len = sizeof(client_addr); client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd == -1) { perror("accept error"); continue; } // 创建子进程处理客户端 pid_t pid = fork(); if (pid == 0) { // 子进程 close(listen_fd); // 子进程不需要监听套接字 handle_client(client_fd, client_addr); } else if (pid > 0) { // 父进程 close(client_fd); // 父进程不需要客户端套接字 } else { perror("fork error"); close(client_fd); } } close(listen_fd); return 0; }

三、多线程版本

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 6000 #define BUFFER_SIZE 128 typedef struct { int client_fd; struct sockaddr_in client_addr; } ClientInfo; void* handle_client(void* arg) { ClientInfo* info = (ClientInfo*)arg; char buffer[BUFFER_SIZE]; printf("客户端连接:%s:%d\n", inet_ntoa(info->client_addr.sin_addr), ntohs(info->client_addr.sin_port)); while (1) { memset(buffer, 0, BUFFER_SIZE); int n = recv(info->client_fd, buffer, BUFFER_SIZE - 1, 0); if (n == 0) { printf("客户端已断开:%s:%d\n", inet_ntoa(info->client_addr.sin_addr), ntohs(info->client_addr.sin_port)); break; } if (n == -1) { perror("recv error"); break; } printf("[%s:%d] %s\n", inet_ntoa(info->client_addr.sin_addr), ntohs(info->client_addr.sin_port), buffer); send(info->client_fd, "OK", 2, 0); } close(info->client_fd); free(info); return NULL; } int main() { int listen_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len; pthread_t tid; // 创建套接字 listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd == -1) { perror("socket error"); exit(1); } // 绑定地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind error"); exit(1); } // 监听 if (listen(listen_fd, 5) == -1) { perror("listen error"); exit(1); } printf("多线程服务器启动成功,端口:%d\n", PORT); while (1) { client_len = sizeof(client_addr); client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd == -1) { perror("accept error"); continue; } ClientInfo* info = (ClientInfo*)malloc(sizeof(ClientInfo)); info->client_fd = client_fd; info->client_addr = client_addr; pthread_create(&tid, NULL, handle_client, info); pthread_detach(tid); // 分离线程,自动回收资源 } close(listen_fd); return 0; }

第四部分:TCP客户端实现

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 6000 #define BUFFER_SIZE 128 int main() { int sock_fd; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE]; // 1. 创建套接字 sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd == -1) { perror("socket error"); exit(1); } // 2. 设置服务器地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本机测试 // 3. 连接服务器 if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("connect error"); close(sock_fd); exit(1); } printf("连接服务器成功\n"); // 4. 循环收发数据 while (1) { printf("请输入消息(输入end退出):"); fgets(buffer, BUFFER_SIZE, stdin); buffer[strlen(buffer) - 1] = '\0'; if (strcmp(buffer, "end") == 0) { break; } send(sock_fd, buffer, strlen(buffer), 0); memset(buffer, 0, BUFFER_SIZE); recv(sock_fd, buffer, BUFFER_SIZE - 1, 0); printf("服务器响应:%s\n", buffer); } // 5. 关闭连接 close(sock_fd); printf("客户端退出\n"); return 0; }

第五部分:常见问题与解决方案

一、僵尸进程问题

在使用多进程版本时,子进程退出后,如果父进程没有调用wait(),会产生僵尸进程。

解决方案1:忽略SIGCHLD信号

signal(SIGCHLD, SIG_IGN);

解决方案2:在信号处理函数中调用wait()

void sigchld_handler(int sig) { while (waitpid(-1, NULL, WNOHANG) > 0); } signal(SIGCHLD, sigchld_handler);

二、端口占用问题

服务器程序退出后,端口不会立即释放,需要等待一段时间(约2分钟)。

解决方案:设置套接字选项SO_REUSEADDR

int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

三、send()返回成功是否保证数据送达?

send()返回成功只表示数据已成功写入发送缓冲区,并不保证对端已经收到。TCP的可靠性保证数据最终会送达,但如果网络断开,数据可能会丢失。

四、recv()返回值含义

返回值含义
>0成功接收的数据字节数
=0对端已关闭连接
=-1发生错误

总结

一、TCP编程核心接口

函数服务端客户端说明
socket()创建套接字
bind()绑定地址
listen()监听连接
accept()接受连接
connect()连接服务器
recv()接收数据
send()发送数据
close()关闭连接

二、TCP三次握手的面试答案

  1. 第一次握手:客户端发送SYN报文(SYN=1, seq=x),进入SYN_SENT状态

  2. 第二次握手:服务器回复SYN+ACK报文(SYN=1, ACK=1, seq=y, ack=x+1),进入SYN_RCVD状态

  3. 第三次握手:客户端回复ACK报文(ACK=1, seq=x+1, ack=y+1),双方进入ESTABLISHED状态

三、高并发服务器实现方案

方案优点缺点
多进程隔离性好,子进程崩溃不影响主进程资源开销大
多线程资源开销小,共享内存方便需要同步,一个线程崩溃可能影响整个进程
IO多路复用单线程处理多个连接编程复杂度高

本文涵盖了TCP网络编程的核心内容:

  1. TCP理论基础:三次握手、四次挥手、头部结构

  2. 编程接口:socket、bind、listen、accept、connect、recv、send

  3. 服务器实现:基础版本、多进程版本、多线程版本

  4. 客户端实现:连接服务器、循环收发数据

  5. 常见问题:僵尸进程、端口占用、高并发

课后作业:

  • 编写并运行多进程和多线程版本的并发服务器

  • 使用telnet或自己编写的客户端测试服务器功能

  • 观察僵尸进程的产生与解决方案的效果

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/4 17:45:27

戴尔服务器风扇噪音终极解决方案:3分钟学会静音控制技巧

戴尔服务器风扇噪音终极解决方案&#xff1a;3分钟学会静音控制技巧 【免费下载链接】dell_fans_controller A tool for control the Dell server fans speed, it sends the control instruction by ipmitool over LAN for Windows, it is a GUI application which is built by…

作者头像 李华
网站建设 2026/5/4 17:45:27

如何批量下载网易云音乐FLAC无损音质:开源工具终极指南

如何批量下载网易云音乐FLAC无损音质&#xff1a;开源工具终极指南 【免费下载链接】NeteaseCloudMusicFlac 根据网易云音乐的歌单, 下载flac无损音乐到本地.。 项目地址: https://gitcode.com/gh_mirrors/nete/NeteaseCloudMusicFlac 还在为无法下载高品质音乐而烦恼吗…

作者头像 李华
网站建设 2026/5/4 17:43:33

牟中县姚家镇国土空间总体规划(2021-2035年)

这份文件是中牟县姚家镇国土空间总体规划&#xff08;2021—2035 年&#xff09;批后公示稿&#xff0c;核心是对姚家镇全域国土空间保护、开发、利用、修复做总体安排&#xff0c;指导镇村建设与空间治理。一、基础信息规划范围&#xff1a;镇域约 53.11 平方公里&#xff0c;…

作者头像 李华
网站建设 2026/5/4 17:42:26

3分钟搞定!浏览器端音乐解锁神器Unlock-Music完全指南

3分钟搞定&#xff01;浏览器端音乐解锁神器Unlock-Music完全指南 【免费下载链接】unlock-music 在浏览器中解锁加密的音乐文件。原仓库&#xff1a; 1. https://github.com/unlock-music/unlock-music &#xff1b;2. https://git.unlock-music.dev/um/web 项目地址: https…

作者头像 李华