使用Socket来建立多个TCP连接是一种常见的网络通信方式,在客户端和服务器之间进行数据交换。需要注意的是,服务器在处理多个客户端时,应该为每个客户端连接创建一个新的线程或进程,或者使用异步IO来避免阻塞。
本文将以多线程和多进程两种方式实现,可以运行多个客户端实例来测试服务器是否能处理多个TCP连接。可以简单地在命令行中多次运行客户端脚本,或者写一个循环来多次创建和关闭客户端连接。
在C语言中,实现一个基本的 TCP 服务器和客户端涉及使用 socket 编程接口,这通常包括 socket()
, bind()
, listen()
, accept()
, send()
, recv()
和 close()
等函数,之前有所介绍。
示例程序
以多线程和多进程两种方式实现,支持多个客户端连接服务器。需要注意的是服务器将监听一个特定的端口,并接受来自客户端的连接。一旦连接建立,它将接收来自客户端的消息,并将其回显给客户端,而客户端的端口绑定时自动分配的,当然也可以修改程序成一个指定端口。
基于多线程支持多个 TCP 连接
server.c
#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 PORT 6666
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#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;
char * read_buf = NULL;
char * write_buf = NULL;
ssize_t count = 0, send_count = 0;
// 初始化
read_buf = malloc(sizeof(char) * BUFFSIZE);
write_buf = malloc(sizeof(char) * BUFFSIZE);
if (!read_buf)
{
printf("服务端读缓存创建异常,断开连接\n");
close(client_fd);
perror("read_buf");
return NULL;
}
if (!write_buf)
{
printf("服务端写缓存创建异常,断开连接\n");
close(client_fd);
perror("read_buf");
return NULL;
}
while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
{
if (count < 0)
{
perror("recv");
}
// 接收数据打印到控制台
printf("从_%d_客户端接收到数据:%s", client_fd, read_buf);
// 把收到的消息写到写缓存
strcpy(write_buf, "收到\n");
send_count = send(client_fd, write_buf, BUFFSIZE, 0);
if (send_count < 0)
{
perror("send");
}
}
// 当客户端输入 ctrl+D 退出循环
// shutdown(client_fd, SHUT_RD);
close(client_fd);
free(read_buf);
free(write_buf);
}
int main(int argc, char const *argv[])
{
struct sockaddr_in server_addr, client_addr;
int sock_fd, tmp_res;
// 清空
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 填写服务端类型
server_addr.sin_family = AF_INET; // ipv4
// 填写IP地址 0.0.0.0
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr);
// 填写端口号
server_addr.sin_port = htons(PORT);
/******** 网络编程 socket 流程 ********/
// 1. 创建socket
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sock_fd);
// 2. 绑定地址
tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("bind", tmp_res);
// 3. 进入监听状态
tmp_res = listen(sock_fd, 128); // 等待队列长度,可以随便填
handle_error("listen", tmp_res);
// 4. 获取客户端的连接 : 需要能够接收多个连接
socklen_t cliaddr_len = sizeof(client_addr);
while (1)
{
int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len);
// 不同的进程访问全局变量而导致的竞态资源的问题
// 全局只创建了一个线程号实例,每次创建一个线程都会覆盖掉之前的线程号
pthread_t pid_read_write;
handle_error("accept", client_fd);
printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
client_fd);
// 和每一个客户端使用一个线程交互 把客户端发送的信息打印到控制台 回复收到
if (pthread_create(&pid_read_write, NULL,
read_from_client_then_write, (void *)&client_fd))
{
perror("pthread_create");
}
// 需要等待线程结束 但是不能挂起等待,否则就无法创建下一个连接
pthread_detach(pid_read_write);
}
printf("释放资源\n");
close(sock_fd);
return 0;
}
client.c
#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 ADDRESS_S ("127.0.0.1")
#define ADDRESS_C ("192.168.100.137")
#define PORT_S 6666
#define PORT_C 8888
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#define handle_error(cmd, result) \
if(result < 0) \
{ \
perror(cmd); \
return -1; \
}
void * read_from_server(void * arg)
{
// 使用recv接收客户端发送的数据 打印到控制台
char * read_buf = NULL;
int client_fd = *(int *)arg; // 得到文件描述符
read_buf = malloc(TYPESIZE);
ssize_t count = 0;
if (!read_buf)
{
perror("malloc_server_read_buf");
return NULL;
}
// 接收数据
// 只要能接受到数据 正常使用 一直挂起
while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
{
if (count < 0)
{
perror("recv");
}
fputs(read_buf, stdout);
}
printf("服务端请求关闭\n");
free(read_buf);
return NULL;
}
void * write_to_server (void * arg)
{
// 接收控制台输入的信息 写出去
char * write_buf = NULL;
int client_fd = *(int *)arg; // 得到文件描述符
write_buf = malloc(TYPESIZE);
ssize_t count = 0;
if (!write_buf)
{
perror("malloc_server_write_buf");
return NULL;
}
while (fgets(write_buf, BUFFSIZE, stdin) != NULL)
{
// 发送数据
count = send(client_fd, write_buf, BUFFSIZE, 0);
if (count < 0)
{
perror("send");
}
}
printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n");
// 可以具体到关闭某一端
shutdown(client_fd, SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
struct sockaddr_in server_addr, client_addr;
int sock_fd, tmp_res;
// 清空
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 填写服务端类型
server_addr.sin_family = AF_INET; // ipv4
// 填写IP地址
inet_pton(AF_INET, ADDRESS_S, &server_addr.sin_addr);
// 填写端口号
server_addr.sin_port = htons(PORT_S);
/******** 网络编程 socket 流程 ********/
// 1. 创建socket,只有一个文件描述符
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sock_fd);
// 2. 绑定 (自动分配)
// 客户端可以不绑定,系统会自动分配一个空闲的端口号给客户端
// 3. 客户端主动连接服务端
tmp_res = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("connect", tmp_res);
printf("连接上服务端_%s:%d\n",
inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));
// 创建子线程用于收消息
pthread_t pid_read, pid_write;
pthread_create(&pid_read, NULL, read_from_server, (void *)&sock_fd);
// 创建子线程用于发消息
pthread_create(&pid_write, NULL, write_to_server, (void *)&sock_fd);
// 阻塞主线程
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
// 释放资源
printf("释放资源\n");
close(sock_fd);
return 0;
}
makefile
server:server.c
- $(CC) -o $@ $^
client:client.c
- $(CC) -o $@ $^
运行结果
基于多进程支持多个 TCP 连接
客户端程序和Makefile文件与上述一致,无需更改,唯一变动的是服务端程序,需要注意的是如何避免僵尸进程,新建进程后关闭不会使用到的资源。
server.c
#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 PORT 6666
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#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;
char * read_buf = NULL;
char * write_buf = NULL;
ssize_t count = 0, send_count = 0;
// 初始化
read_buf = malloc(sizeof(char) * BUFFSIZE);
write_buf = malloc(sizeof(char) * BUFFSIZE);
if (!read_buf)
{
printf("服务端读缓存创建异常,断开连接\n");
close(client_fd);
perror("read_buf");
return NULL;
}
if (!write_buf)
{
printf("服务端写缓存创建异常,断开连接\n");
close(client_fd);
perror("read_buf");
return NULL;
}
while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
{
if (count < 0)
{
perror("recv");
}
// 接收数据打印到控制台
printf("从_%d_客户端接收到数据:%s", client_fd, read_buf);
// 把收到的消息写到写缓存
strcpy(write_buf, "收到\n");
send_count = send(client_fd, write_buf, BUFFSIZE, 0);
if (send_count < 0)
{
perror("send");
}
}
// 当客户端输入 ctrl+D 退出循环
// shutdown(client_fd, SHUT_RD);
close(client_fd);
free(read_buf);
free(write_buf);
}
/**
* 关闭回收所有的子进程,避免僵尸进程
*/
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);
}
}
}
int main(int argc, char const *argv[])
{
struct sockaddr_in server_addr, client_addr;
int sock_fd, tmp_res;
// 注册信号处理函数 回收子进程的资源 SIGCLD
signal(SIGCLD, zombie_dealer);
// 清空
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 填写服务端类型
server_addr.sin_family = AF_INET; // ipv4
// 填写IP地址 0.0.0.0
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr);
// 填写端口号
server_addr.sin_port = htons(PORT);
/******** 网络编程 socket 流程 ********/
// 1. 创建socket
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sock_fd);
// 2. 绑定地址
tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("bind", tmp_res);
// 3. 进入监听状态
tmp_res = listen(sock_fd, 128); // 等待队列长度,可以随便填
handle_error("listen", tmp_res);
// 4. 获取客户端的连接 : 需要能够接收多个连接
socklen_t cliaddr_len = sizeof(client_addr);
while (1)
{
int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len);
// 不同的进程访问全局变量而导致的竞态资源的问题
// 全局只创建了一个线程号实例,每次创建一个线程都会覆盖掉之前的线程号
pthread_t pid_read_write;
handle_error("accept", client_fd);
// 创建单独的子进程和连接的客户端交互
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
} else if (pid == 0)
{
// 子进程
// 关闭不会使用的sock_fd
close(sock_fd);
printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
client_fd);
// 从客户端读取数据 并回复
read_from_client_then_write((void *)&client_fd);
// 关闭 client_fd
close(client_fd);
exit(EXIT_SUCCESS);
} else
{
// 父进程
// 关闭掉不使用的client_fd
close(client_fd);
}
}
printf("释放资源\n");
close(sock_fd);
return 0;
}
运行结果
可以看到两个连接所创建的 client_fd
文件描述符都是4,是因为每次创建子进程后,父进程就会释放 client_fd
,对应的文件描述符也就被释放,新的连接被创建时则会从可用的最小文件描述符开始占用,因此新的 client_fd
也是4。虽然 client_fd
是一样的,但对应的底层文件描述却是完全不同的。
赞