引言
本文需掌握前两篇博客内容:Linux 网络编程:深度理解网络字节序与主机字节序、大端字节序与小端字节序-CSDN博客
Linux网络编程:Socket套接字编程概念及常用API接口介绍-CSDN博客
了解了 Socket 编程API介绍及网络字节序的转换,接下来就可以入门 TCP 协议的 Socket 网络通讯了,本文将会介绍基本的 TCP 协议下socket 编程流程,实现单个服务端-客户端连接模型、基于多线程、多进程实现的支持多个连接的服务端模型。依旧是作为笔者的学习笔记,为巩固大家的学习成果同时方便之后的复习,话不多说开始吧!
声明:本文借鉴了尚硅谷的 Linux 应用层开发课程内容及文档。
一、TCP协议的 Socket 编程基本流程
TCP socket 网络编程的核心在于Client-Server(客户端-服务器)模型。我们都知道:在Linux哲学中,"一切皆文件",因此Socket本质上也是一种文件描述符(File Descriptor, fd)。
在编写代码之前,我们需要先在脑海中建立起这个流程图。注意:我们学习计算机网络时了解过网络的分层概念,TCP的三次握手(3-way handshake)在这个过程中由操作系统内核自动完成的,我们在编码阶段其实并不需要关注三次握手是如何建立的,这就是分层的好处。
下面我将从服务端 和客户端两个角度,按顺序详细讲解一个 TCP 连接的建立流程。
1.1 服务端流程(Server-Side)
服务端的核心任务是:
-
socket(): 创建一个socket(买个手机)。 -
bind(): 绑定IP和端口(插上电话卡,有了号码)。 -
listen(): 开启监听模式(等待电话响)。 -
accept(): 阻塞等待,接收客户端连接(接听电话)。 -
send/recv(): 进行通讯(与客户端通话) -
close(): 通讯关闭(挂断电话)
典型流程如下图:
socket() → bind() → listen() → accept() → send/recv() → close()
① socket() ------ 创建监听套接字
cpp
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
AF_INET 说明时IPv4地址,SOCK_STREAM: 面向 TCP 建立连接,返回值是监听 socket 的文件描述符
注意:它不是通信用的 socket,而是"接客"的 socket。
② bind() ------ 绑定 IP 和端口
之前的文章讲过 sockaddr_in 和 htonl/htons,这里不再展开。
目的:告诉内核"我监听的是哪个 IP + 端口"。
若不 bind,内核默认分配随机端口,那就无法作为服务端使用。
bind() 正确示例
cpp
struct sockaddr_in addr;
addr.sin_family = AF_INET;
// 端口号转网络字节序
addr.sin_port = htons(8080);
// IP 地址转网络字节序
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
③ listen() ------ 让 socket 进入监听状态
cpp
listen(sockfd, backlog);
作用:将 socket 变为被动监听 socket ,backlog 设置内核连接队列大小
监听 socket 不参与数据传输,只负责"接收连接请求"。
④ accept() ------ 接受客户端连接(三次握手后)
cpp
int client_fd = accept(sockfd, NULL, NULL);
accept() 是一个很重要的阻塞函数,特点:
-
阻塞等待客户端连接
-
返回一个新的 client_fd,同样是一个文件描述符
-
client_fd 用来和客户端通信
-
原本的 sockfd 继续用于 accept,多次 accept 就能接多个连接,建立多个 client_fd
⑤ send()/recv() ------ 进行数据传输
cpp
recv(sockfd, read_buf, 1024, 0);
send(sockfd, write_buf, 1024, 0);
此处才是真正的收发数据过程。
⑥ close() ------ 关闭连接
客户端或服务端任意一方关闭,连接就断开。
服务端一般:
-
关闭 connfd(每个客户一个)
-
程序退出时关闭 sockfd
1.2 客户端流程(Client-Side)
客户端的流程更简单:
-
socket(): 创建一个socket(买个手机)。 -
connect(): 发起连接请求(拨打电话)。 -
send/recv(): 进行通讯(与服务端通话) -
close(): 通讯关闭(挂断电话)
socket() → connect() → send/recv() → close()
1.3 单个TCP socket 连接示例
下面给出单个 socket 连接的示例代码:
服务端 (single_conn_server.c) :
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
void *read_from_client(void* arg)
{
int client_fd = *(int*) arg;
char* read_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!read_buf)
{
perror("malloc server read_buf");
return (void*)1;
}
// 接收client_fd文件描述符下的文件的内存
// 只要能接收到数据,就正常使用,一直挂起
while (count = recv(client_fd,read_buf,1024,0))
{
if (count < 0)
{
// 错误输出
perror("count");
}
/* code */
fputs(read_buf,stdout);
}
// 线程执行到这说明循环结束,客户端已经关闭了请求
printf("客户端请求关闭\n");
// 释放内存
free(read_buf);
return NULL;
}
void* write_to_client(void* arg)
{
int client_fd = *(int*) arg;
char* write_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!write_buf)
{
perror("malloc server read_buf");
return (void*)1;
}
while (fgets(write_buf,1024,stdin) != NULL)
{
/*发送数据*/
count = send(client_fd,write_buf,1024,0);
if (count < 0)
{
perror("send");
}
}
shutdown(client_fd,SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
pthread_t pid_read,pid_write;
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务段地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 INADDR_ANY就是0.0.0.0
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 填写端口号
server_addr.sin_port = htons(6666);
// 网络编程流程介绍
// 1. socket套结字创建,SOCK_STREAM说明使用的流传输,是TCP协议
int sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 2. 绑定地址
int temp_result = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
handle_error("bind",temp_result);
// 3. 进入监听状态
temp_result = listen(sockfd,128);
handle_error("listen",temp_result);
// 4. 获取客户端的连接
socklen_t cliaddr_len = sizeof(client_addr);
// 返回的文件描述符才是能够和客户端通讯的文件描述符
int clientfd = accept(sockfd,(struct sockaddr*)&client_addr,&cliaddr_len);
handle_error("accept",clientfd);
printf("与客户端%s %d建立连接 文件描述符是%d\n",inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),clientfd);
pthread_create(&pid_read,NULL,read_from_client,&clientfd);
pthread_create(&pid_write,NULL,write_to_client,&clientfd);
pthread_join(pid_read,NULL);
pthread_join(pid_write,NULL);
printf("释放资源\n");
close(sockfd);
close(clientfd);
return 0;
}
客户端 (single_conn_client.c) :
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
void *read_from_server(void* arg)
{
int client_fd = *(int*) arg;
char* read_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!read_buf)
{
perror("malloc server read_buf");
return NULL;
}
// 接收client_fd文件描述符下的文件的内存
// 只要能接收到数据,就正常使用,一直挂起
while (count = recv(client_fd,read_buf,1024,0))
{
if (count < 0)
{
// 错误输出
perror("count");
}
/* code */
fputs(read_buf,stdout);
}
// 线程执行到这说明循环结束,客户端已经关闭了请求
printf("服务端请求关闭\n");
// 释放内存
free(read_buf);
return NULL;
}
void* write_to_server(void* arg)
{
int client_fd = *(int*) arg;
char* write_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!write_buf)
{
perror("malloc server read_buf");
return NULL;
}
while (fgets(write_buf,1024,stdin) != NULL)
{
/*发送数据*/
count = send(client_fd,write_buf,1024,0);
if (count < 0)
{
perror("send");
}
}
shutdown(client_fd,SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
pthread_t pid_read,pid_write;
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务端地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 本机地址127.0.0.1
server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
// 填写端口号 6666 这里端口号需要与服务端的端口号对应上
server_addr.sin_port = htons(6666);
// 填写客户端地址,指定协议ipv4
client_addr.sin_family = AF_INET;
// 填写ip地址
inet_pton(AF_INET,"192.168.200.131",&client_addr.sin_addr.s_addr);
// 填写端口号
client_addr.sin_port = htons(8888);
// 客户端网络编程流程介绍
// 1. socket套结字创建,SOCK_STREAM说明使用的流传输,是TCP协议
int sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 2. 绑定地址
int temp_result = bind(sockfd,(struct sockaddr*)&client_addr,sizeof(client_addr));
handle_error("bind",temp_result);
// 3. 主动连接服务端
temp_result = connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
handle_error("connect",temp_result);
printf("连接上服务端%s %d\n",inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port));
pthread_create(&pid_read,NULL,read_from_server,&sockfd);
pthread_create(&pid_write,NULL,write_to_server,&sockfd);
pthread_join(pid_read,NULL);
pthread_join(pid_write,NULL);
printf("释放资源\n");
close(sockfd);
return 0;
}
案例测试:
打开 Xshell 工具进行远程测试:

我已经用 Makefile 文件进行了可执行性目标文件的生成,我们直接进行测试:

可以看到客户端与服务端已经建立起了连接,我们发送数据看连接是否稳定:

可以看到 TCP 协议建立的是可靠稳定的传输服务,在单个连接的情况下服务端与客户端都可以进行相互通讯发送消息,如果还想更加直观地看到二者相互通讯的过程,我们还可在上述文件的接收消息线程中,打印二者的文件描述符、二者的IP地址等信息。
在两端发送消息的线程函数中,我还添加了shutdown来测试TCP四次挥手的特性:
cpp
shutdown(client_fd,SHUT_WR);
当我们在客户端或者是服务端按下 Ctrl+D,断开连接时:

这张是客户端按下 Ctrl+D,断开连接,客户端关闭读写功能,不能向客户端发送数据,但是连接并未就此断开,服务端的发送数据功能还在继续,客户端还能接收到服务端的内容:

服务端也是同理。
二、服务端如何支持多个客户端连接?
真实世界中,服务器通常需要同时服务多个客户端。例如:
-
Web 服务器需同时响应多个浏览器请求。
-
聊天服务器需广播消息给所有在线用户;。
-
游戏服务器需实时同步多个玩家状态。
单连接模型只能处理一个客户端 ,一旦 accept() 获得 client_fd,就要一直 recv()/send() ,不可能继续处理新连接。
由于 accept()是阻塞的,同时而 recv() 也是阻塞的。若不引入并发机制,后续连接将被"饿死"。
为每个客户端连接创建独立的执行上下文,也就是进程或者是线程 ,让主线程专注于 accept()。
三、多进程实现多连接服务端
多进程模型是最经典的方式,每 accept 一个连接就 fork 一个子进程,让它负责通信。
3.1 关键思路
cpp
while (1) {
client_fd = accept();
pid = fork();
if (pid == 0) { // 子进程
close(sockfd); // 关闭监听socket
handle(client_fd); // 处理客户端通信
exit(0);
}else if(pid > 0){ // 父进程
close(client_fd); // 不需要处理子进程内容,关闭连接socket
}
close(connfd); // 父进程关闭通信socket
}
服务端代码实现:
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
void zombie_dealer(int sig) {
pid_t pid;
int status;
// 一个SIGCHLD可能对应多个子进程的退出
// 使用while循环回收所有退出的子进程,避免僵尸进程的出现
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("子进程: %d 以 %d 状态正常退出,已被回收\n", pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程: %d 被 %d 信号杀死,已被回收\n", pid, WTERMSIG(status));
} else {
printf("子进程: %d 因其它原因退出,已被回收\n", pid);
}
}
}
void *read_from_client_then_write(void* arg)
{
int client_fd = *(int*)arg;
ssize_t count = 0,send_count = 0;
char* read_buf = NULL;
char* write_buf = NULL;
read_buf = malloc(sizeof(char) * 1024);
// 判断内存是否分配成功
if (!read_buf)
{
printf("服务端读缓存创建异常 断开连接\n");
shutdown(client_fd,SHUT_WR);
close(client_fd);
perror("malloc server read_buf");
return NULL;
}
write_buf = malloc(sizeof(char) * 1024);
// 判断内存是否分配成功
if (!write_buf)
{
printf("服务端写缓存创建异常 断开连接\n");
shutdown(client_fd,SHUT_WR);
close(client_fd);
perror("malloc server write_buf");
return NULL;
}
while ((count = recv(client_fd,read_buf,1024,0)))
{
// recv获取失败返回-1
if (count < 0)
{
perror("recv");
}
// 接收数据打印到控制台
printf("receive message from client_fd %d:\n %s\n",client_fd,
read_buf);
// 收到数据的消息写到写缓存
strcpy(write_buf,"received~\n");
send_count = send(client_fd,write_buf,102,0);
if (send_count < 0)
{
perror("send");
}
}
// 客户端输入ctrl+d时退出循环
// shutdown(client_fd,SHUT_RD);
printf("客户端client_fd %d 请求断开连接",client_fd);
close(client_fd);
free(write_buf);
free(read_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
// 定义socket的文件描述符
int sockfd,temp_result;
// 注册信号处理函数 SIGCHLD 正确回收所有的子进程
signal(SIGCLD,zombie_dealer);
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务段地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 INADDR_ANY就是0.0.0.0
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 填写端口号
server_addr.sin_port = htons(6666);
// 网络编程流程介绍
// 1. socket套结字创建,SOCK_STREAM说明使用的流传输,是TCP协议
sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 绑定服务端地址
temp_result = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
handle_error("bind",temp_result);
// 3.服务端进入监听状态
temp_result = listen(sockfd,128);
handle_error("listen",temp_result);
// 4.获取多个客户端连接
socklen_t clientaddr_len = sizeof(client_addr);
while (1)
{
int clientfd = accept(sockfd,(struct sockaddr*)&client_addr,&clientaddr_len);
handle_error("accept",clientfd);
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
}else if(pid == 0)
{
// 子进程
// 关闭不会使用sockfd
close(sockfd);
printf("与客户端%s %d 建立连接%d\n",inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),clientfd);
// 进行通讯
read_from_client_then_write((void*)&clientfd);
// 通讯完毕关闭文件描述符clientfd
close(clientfd);
exit(EXIT_SUCCESS);
}else
{
// 父进程
// 关闭调不使用的clientfd
close(clientfd);
}
}
printf("释放资源\n");
close(sockfd);
return 0;
}
由于这里是多连接的服务端,其主要的任务是接收来自多个客户端的数据内容,我们并不实现针对于某一个客户端的单一通讯,定义一个数据收发函数,同一处理每一条来自于不同客户端的数据。
客户端代码实现:
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
void *read_from_server(void* arg)
{
int client_fd = *(int*) arg;
char* read_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!read_buf)
{
perror("malloc server read_buf");
return NULL;
}
// 接收client_fd文件描述符下的文件的内存
// 只要能接收到数据,就正常使用,一直挂起
while (count = recv(client_fd,read_buf,1024,0))
{
if (count < 0)
{
// 错误输出
perror("count");
}
/* code */
fputs(read_buf,stdout);
}
// 线程执行到这说明循环结束,客户端已经关闭了请求
printf("服务端请求关闭\n");
// 释放内存
free(read_buf);
return NULL;
}
void* write_to_server(void* arg)
{
int client_fd = *(int*) arg;
char* write_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!write_buf)
{
perror("malloc server read_buf");
return NULL;
}
while (fgets(write_buf,1024,stdin) != NULL)
{
/*发送数据*/
count = send(client_fd,write_buf,1024,0);
if (count < 0)
{
perror("send");
}
}
shutdown(client_fd,SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
pthread_t pid_read,pid_write;
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务端地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 本机地址127.0.0.1
server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
// 填写端口号 6666 这里端口号需要与服务端的端口号对应上
server_addr.sin_port = htons(6666);
// 填写客户端地址,指定协议ipv4
client_addr.sin_family = AF_INET;
// 填写ip地址
inet_pton(AF_INET,"192.168.200.131",&client_addr.sin_addr.s_addr);
// 填写端口号
client_addr.sin_port = htons(8888);
// 客户端网络编程流程介绍
// 1. socket套结字创建,SOCK_STREAM说明使用的流传输,是TCP协议
int sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 2. 绑定地址
// 客户端主机会自动分配端口号,这里注释掉绑定端口号,是避免多个客户端占用同一个端口
// temp_result = bind(sockfd,(struct sockaddr*)&client_addr,sizeof(client_addr));
// handle_error("bind",temp_result);
// 3. 主动连接服务端
int temp_result = connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
handle_error("connect",temp_result);
printf("连接上服务端%s %d\n",inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port));
pthread_create(&pid_read,NULL,read_from_server,&sockfd);
pthread_create(&pid_write,NULL,write_to_server,&sockfd);
pthread_join(pid_read,NULL);
pthread_join(pid_write,NULL);
printf("释放资源\n");
close(sockfd);
return 0;
}
客户端的实现比较简单,只需要实现对应的连接,以及相应的读写函数即可,与单个socket连接的client客户端类似。
测试:
我们在 Xshell 远程开启三个窗口进行测试一个服务端与两个客户端。
使用 gcc 命令生成了可执行性文件之后我们进行测试:

可以看到,服务端成功连接上了两个客户端,同时两个客户端的socket在服务端是不同的。

服务端可以接收到不同客户端的消息。
3.2 多进程优缺点
⭐ 优点
-
稳定可靠,一个进程崩不会影响其他连接
-
利用多核 CPU(每个子进程独立运行)
-
编程简单
❗ 缺点
-
fork 开销较大(复制页表、调度成本)
-
进程之间通信困难
-
大规模连接不适合(建议用 epoll)
适用于:几十到几百连接的服务端。
四、多线程实现多连接服务端
相比多进程,多线程模型更轻量、内存开销更小,更适用于高并发。
4.1 关键思路
每次 accept()接收到一个 client 客户端就直接创建一个对应的客户端线程函数:
cpp
while (1) {
connfd = accept();
pthread_create(&tid, NULL, thread_func, (void*)connfd);
}
线程函数:
cpp
void* thread_func(void *arg) {
int connfd = (int)arg;
handle(connfd);
close(connfd);
return NULL;
}
每个线程负责与一个客户端通信。
下面给出服务端的代码示例:
服务端代码实现:
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
void *read_from_client_then_write(void* arg)
{
int client_fd = *(int*)arg;
ssize_t count = 0,send_count = 0;
char* read_buf = NULL;
char* write_buf = NULL;
read_buf = malloc(sizeof(char) * 1024);
// 判断内存是否分配成功
if (!read_buf)
{
printf("服务端读缓存创建异常 断开连接\n");
shutdown(client_fd,SHUT_WR);
close(client_fd);
perror("malloc server read_buf");
return NULL;
}
write_buf = malloc(sizeof(char) * 1024);
// 判断内存是否分配成功
if (!write_buf)
{
printf("服务端写缓存创建异常 断开连接\n");
shutdown(client_fd,SHUT_WR);
close(client_fd);
perror("malloc server write_buf");
return NULL;
}
while ((count = recv(client_fd,read_buf,1024,0)))
{
// recv获取失败返回-1
if (count < 0)
{
perror("recv");
}
// 接收数据打印到控制台
printf("receive message from client_fd %d:\n %s\n",client_fd,
read_buf);
// 收到数据的消息写到写缓存
strcpy(write_buf,"received~\n");
send_count = send(client_fd,write_buf,102,0);
if (send_count < 0)
{
perror("send");
}
}
// 客户端输入ctrl+d时退出循环
// shutdown(client_fd,SHUT_RD);
printf("客户端client_fd %d 请求断开连接",client_fd);
close(client_fd);
free(write_buf);
free(read_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
// 定义socket的文件描述符
int sockfd,temp_result;
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务段地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 INADDR_ANY就是0.0.0.0
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 填写端口号
server_addr.sin_port = htons(6666);
// 网络编程流程介绍
// 1. socket套结字创建,SOCK_STREAM说明使用的流传输,是TCP协议
sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 绑定服务端地址
temp_result = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
handle_error("bind",temp_result);
// 3.服务端进入监听状态
temp_result = listen(sockfd,128);
handle_error("listen",temp_result);
// 4.获取多个客户端连接
socklen_t clientaddr_len = sizeof(client_addr);
while (1)
{
int clientfd = accept(sockfd,(struct sockaddr*)&client_addr,&clientaddr_len);
handle_error("accept",clientfd);
printf("与客户端%s %d建立连接 文件描述符是%d\n",inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),clientfd);
// 进行通讯的线程
pthread_t pid_read_write;
if (pthread_create(&pid_read_write,NULL,read_from_client_then_write,(void*)&clientfd))
{
perror("pthread_create");
}
// 需要的等待线程结束,但是不能挂起
pthread_detach(pid_read_write);
}
printf("释放资源\n");
close(sockfd);
return 0;
}
可以看到多线程服务端与多进程服务端的代码实现上,不同在于其 main 函数下实现逻辑不同,这里多线程采用每一 socket 对应一个 pthread 进行读写操作。其中的读写线程函数逻辑大差不差。
客户端代码实现:
cpp
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
} \
void *read_from_server(void* arg)
{
int client_fd = *(int*) arg;
char* read_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!read_buf)
{
perror("malloc server read_buf");
return NULL;
}
// 接收client_fd文件描述符下的文件的内存
// 只要能接收到数据,就正常使用,一直挂起
while (count = recv(client_fd,read_buf,1024,0))
{
if (count < 0)
{
// 错误输出
perror("count");
}
/* code */
fputs(read_buf,stdout);
}
// 线程执行到这说明循环结束,客户端已经关闭了请求
printf("服务端请求关闭\n");
// 释放内存
free(read_buf);
return NULL;
}
void* write_to_server(void* arg)
{
int client_fd = *(int*) arg;
char* write_buf = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if (!write_buf)
{
perror("malloc server read_buf");
return NULL;
}
while (fgets(write_buf,1024,stdin) != NULL)
{
/*发送数据*/
count = send(client_fd,write_buf,1024,0);
if (count < 0)
{
perror("send");
}
}
shutdown(client_fd,SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
pthread_t pid_read,pid_write;
// 定义客户端与服务端的套结字数据结构,供之后填写
struct sockaddr_in server_addr,client_addr;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 填写服务端地址,指定协议
server_addr.sin_family = AF_INET;
// 填写ip地址 本机地址127.0.0.1
server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
// 填写端口号 6666 这里端口号需要与服务端的端口号对应上
server_addr.sin_port = htons(6666);
// 填写客户端地址,指定协议ipv4
client_addr.sin_family = AF_INET;
// 填写ip地址
inet_pton(AF_INET,"192.168.200.131",&client_addr.sin_addr.s_addr);
// 填写端口号
client_addr.sin_port = htons(8888);
// 客户端网络编程流程介绍
// 1. socket套结字创建,SOCK_STREAM说明使用的流传输,是TCP协议
int sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 2. 绑定地址
// 客户端主机会自动分配端口号,这里注释掉绑定端口号,是避免多个客户端占用同一个端口
// temp_result = bind(sockfd,(struct sockaddr*)&client_addr,sizeof(client_addr));
// handle_error("bind",temp_result);
// 3. 主动连接服务端
int temp_result = connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
handle_error("connect",temp_result);
printf("连接上服务端%s %d\n",inet_ntoa(server_addr.sin_addr),
ntohs(server_addr.sin_port));
pthread_create(&pid_read,NULL,read_from_server,&sockfd);
pthread_create(&pid_write,NULL,write_to_server,&sockfd);
pthread_join(pid_read,NULL);
pthread_join(pid_write,NULL);
printf("释放资源\n");
close(sockfd);
return 0;
}
客户端的代码其实与基于多进程实现的客户端基本一致的。
测试:
开启服务端

客户端连接服务端

可以看到两个客户端在服务端的socket文件描述符是不同的,同时三者之间进行通讯

4.2 多线程优缺点
⭐ 优点
-
线程创建成本比进程低
-
数据共享容易(同一进程)
-
性能高
-
更现代的大多数 TCP 服务器都用线程+epoll
❗ 缺点
-
线程共享内存 → 容易出现竞争条件
-
锁复杂度提升
-
某线程崩溃可能导致整个进程崩溃
适用于:中等规模连接 / 并发较高但不至于几十万连接的服务端。
五、 多进程 vs 多线程:该如何选择?
在面试或实际开发中,对比这两种模型是有必要的。
| 特性 | 多进程 (Multi-process) | 多线程 (Multi-thread) |
|---|---|---|
| 资源开销 | 高。每个进程有独立的地址空间,创建和销毁开销大。 | 低。共享进程地址空间,创建快,切换快。 |
| 稳定性 | 强。一个子进程崩了(SegFault),不会影响主进程和其他子进程。 | 弱。一个线程崩了,整个进程(包括所有其他线程)都会挂掉。 |
| 数据共享 | 难。需要通过 IPC(管道、共享内存、消息队列)进行通信。 | 易 。全局变量、堆内存共享,但需要加锁(Mutex/Spinlock)。 |
| 适用场景 | 客户端连接数不多,但业务逻辑复杂、安全性要求高(如 Nginx 的 Worker 进程)。 | 客户端连接数较多,轻量级任务,追求高性能(如 Memcached)。 |
结语
从单连接到多连接,我们完成了 TCP 服务器的一次关键进化。多进程与多线程模型虽"古老",却是理解现代异步 I/O(如 epoll、IOCP)的前提。只有亲手写过 fork 和 pthread,才能真正体会到"并发"的代价与魅力。
原创不易,转载请注明出处。
点赞 + 收藏 + 关注,获取更多 Linux/网络/系统编程干货!