引言
网络编程是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()
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
| 参数 | 常用值 | 说明 |
|---|---|---|
domain |
AF_INET |
IPv4地址族 |
AF_INET6 |
IPv6地址族 | |
type |
SOCK_STREAM |
TCP流式服务 |
SOCK_DGRAM |
UDP数据报服务 | |
protocol |
0 |
默认协议 |
返回值: 成功返回套接字描述符(类似文件描述符),失败返回-1。
二、绑定地址------bind()
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于将套接字与指定的IP地址和端口号绑定。
cpp
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()
cpp
int listen(int sockfd, int backlog);
backlog参数指定已完成握手队列的最大长度。
注意:
-
Linux系统中,
backlog表示已完成握手队列的长度 -
某些Unix系统中,
backlog表示未完成+已完成握手队列的总长度 -
现代内核中队列长度已大幅增加
四、接受连接------accept()
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
从已完成握手队列中取出一个连接,返回一个新的套接字描述符,用于与客户端通信。
阻塞特性: 如果队列为空,accept()会阻塞等待。
五、连接服务器------connect()
cpp
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端使用此函数向服务器发起连接。
六、收发数据------recv()/send()
cpp
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,表示无特殊选项。
七、字节序转换函数
网络中统一使用大端字节序(网络字节序),需要进行转换:
cpp
#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服务器实现
一、基础版本(单线程)
cpp
#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()会阻塞,导致无法同时处理多个客户端。使用多进程可以解决这个问题:
cpp
#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;
}
三、多线程版本
cpp
#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客户端实现
cpp
#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信号
cpp
signal(SIGCHLD, SIG_IGN);
解决方案2:在信号处理函数中调用wait()
cpp
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
signal(SIGCHLD, sigchld_handler);
二、端口占用问题
服务器程序退出后,端口不会立即释放,需要等待一段时间(约2分钟)。
解决方案: 设置套接字选项SO_REUSEADDR
cpp
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三次握手的面试答案
-
第一次握手:客户端发送SYN报文(SYN=1, seq=x),进入SYN_SENT状态
-
第二次握手:服务器回复SYN+ACK报文(SYN=1, ACK=1, seq=y, ack=x+1),进入SYN_RCVD状态
-
第三次握手:客户端回复ACK报文(ACK=1, seq=x+1, ack=y+1),双方进入ESTABLISHED状态
三、高并发服务器实现方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| 多进程 | 隔离性好,子进程崩溃不影响主进程 | 资源开销大 |
| 多线程 | 资源开销小,共享内存方便 | 需要同步,一个线程崩溃可能影响整个进程 |
| IO多路复用 | 单线程处理多个连接 | 编程复杂度高 |
本文涵盖了TCP网络编程的核心内容:
-
TCP理论基础:三次握手、四次挥手、头部结构
-
编程接口:socket、bind、listen、accept、connect、recv、send
-
服务器实现:基础版本、多进程版本、多线程版本
-
客户端实现:连接服务器、循环收发数据
-
常见问题:僵尸进程、端口占用、高并发
课后作业:
-
编写并运行多进程和多线程版本的并发服务器
-
使用telnet或自己编写的客户端测试服务器功能
-
观察僵尸进程的产生与解决方案的效果