小目标3:如何实现多个客户端的连接

发布时间 2023-10-05 13:52:44作者: †CS.Renascence

小目标3:如何实现多个客户端的连接

如果有不止一个客户端连接入的话,之前的代码是无法解决这个问题的

原因:

如果多个客户端尝试连接,后续连接将阻塞在 accept 函数上,等待服务器处理当前连接的循环结束才行。

 accept_socket = accept(server_socket, NULL, NULL);
 printf("有客户端连接到服务器!\n");
 ​
 while (1) //服务器将持续接收和发送数据,直到手动停止程序。
 {
     //read函数就是接受客户端发来的数据,存储到buffer里面,返回值表示实际上从accept_socket那边读取到的字节数
 ​
     res = read(accept_socket, buffer, sizeof(buffer));
     printf("client read %s\n", buffer);
     //向accept_socket写入buffer中数据,写的数据的字节数为res
     write(accept_socket, buffer, res);
     memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
 }

解决思路:多线程编程,把while(1)这个循环代码放到另外一个线程执行,然后accept_socket放在主线程

修改服务器部分的代码

多线程补充的头文件

 #include<pthread.h>
 //C/C++中用于引入多线程编程支持的头文件。pthread 是 POSIX 线程的缩写,它定义了一组函数和数据类型,用于创建、管理和同步线程

 

调用函数pthread_create—创建一个线程

pthread_create到底怎么使用呢?

在Linux终端中输入

 man pthread_create//查看关于 pthread_create 函数的手册页,以获取详细的文档和信息

返回内容

 NAME
        pthread_create - create a new thread//用途是创建线程
 ​
 SYNOPSIS
        #include <pthread.h>//需要的头文件
 ​
        int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                           void *(*start_routine) (void *), void *arg);
 ​
        Compile and link with -pthread.

参数解释:

  1. pthread_t *thread:这是一个指向 pthread_t 类型的指针,用于存储新线程的标识符。一旦线程创建成功,pthread_create 函数会将线程的标识符存储在这个指针所指向的内存中。

  2. const pthread_attr_t *attr:这是一个指向 pthread_attr_t 类型的指针,用于指定线程的属性。通常情况下,可以将其设置为 **NULL,表示使用默认属性**。如果你需要指定线程的属性,可以使用 pthread_attr_init 函数初始化一个线程属性对象,然后将其传递给 pthread_create

  3. void *(*start_routine) (void *):这是一个函数指针,指向一个函数,这个函数是新线程的入口点(也就是新线程要执行的函数)。这个函数必须接受一个 void * 类型的参数,并返回一个 void * 类型的指针。新线程将从这个函数开始执行。

  4. void *arg:这是一个 void * 类型的指针,向新线程传递需要处理的数据或信息。这里放套接字的地址

代码修改

首先我们把accept_socket代码部分放到while(1)循环里面,每一次循环都检查有没有新的客户端接入,如果有的话就调用创建线程函数

 while (1) //服务器将持续接收和发送数据,直到手动停止程序。
 {
     accept_socket = accept(server_socket, NULL, NULL);
     printf("有客户端连接到服务器!\n");
     res = read(accept_socket, buffer, sizeof(buffer));
     printf("client read %s\n", buffer);
     //向accept_socket写入buffer中数据,写的数据的字节数为res
     write(accept_socket, buffer, res);
     memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
 }

接下来开始书写线程函数pthread_create,在printf("有客户端连接到服务器!\n");后面接上线程函数

定义一个线程编号,第一个参数用

 pthread_t thread_id;//定义一个线程编号

第二个参数放NULL

第三个参数是线程函数(这里还没有定义,先写函数名)

第四个参数是接收到客户端连接的套接字描述符的地址

 pthread_create(&thread_id, NULL, thread_fun, &accept_socket);
 //创建一个新的线程,创建成功之后,系统会执行thread_fun代码(多线程代码),主线程代码也会继续执行

 

然后我们开始书写线程函数,每次创建线程都会执行的内容

 //放在man函数前面,注意参数和返回类型
 void* thread_fun(void* arg) {
     while(1){
     res = read(accept_socket, buffer, sizeof(buffer));
     printf("client read %s\n", buffer);
     //向accept_socket写入buffer中数据,写的数据的字节数为res
     write(accept_socket, buffer, res);
     memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
     }
 }

但是这里会报错,因为这个函数在前面,有很多变量在这个位置还是没有定义的,所以我们重新定义一下变量,修改后的线程函数如下:

 void* thread_fun(void* arg) {
     int acpt_socket = *(int*)arg;
     int res;
     char buffer[50];
     while (1) {
         res = read(acpt_socket, buffer, sizeof(buffer));
         printf("client read %s\n", buffer);
         //向accept_socket写入buffer中数据,写的数据的字节数为res
         write(acpt_socket, buffer, res);
         memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
     }
 }

 

如果现在运行会报错:“对pthread_create未定义的引用”

解决方式:

在解决方案窗口项目文件夹处右键:“属性——C/C++——命令行"

在其他选项的框框里面输入

 -pthread

即可编译过关!!

 

完整服务器运行代码

 #include<cstdio>//C++标准库的头文件
 #include<unistd.h>//Unix标准头文件
 #include<sys/types.h>//这个头文件定义了各种系统相关的数据类型
 #include<sys/socket.h>//这个头文件用于网络编程,包含了与套接字(socket)相关的函数和数据结构的声明
 #include<arpa/inet.h>//通常用于处理IP地址和套接字地址的转换
 #include<string.h>//字符串头文件,因为后面有用到memset
 #include<pthread.h>//线程相关
 ​
 void* thread_fun(void* arg) {
     int acpt_socket = *(int*)arg;
     int res;
     char buffer[50];
     while (1) {
         res = read(acpt_socket, buffer, sizeof(buffer));
         printf("client read %s\n", buffer);
         //向accept_socket写入buffer中数据,写的数据的字节数为res
         write(acpt_socket, buffer, res);
         memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
     }
     
 }
 ​
 int main() {
 ​
     //创建一个套接字描述符
     int server_socket;//这是一个唯一标识套接字的整数
     server_socket = socket(AF_INET, SOCK_STREAM, 0);
     /*
    创建了一个套接字,并将其文件描述符存储在 server_socket 变量中
    AF_INET 表示IPv4地址族
    SOCK_STREAM: 这是套接字类型,表示创建的套接字将使用面向连接的TCP协议
    0: 这是套接字的协议参数,通常设置为0
    */
 ​
     struct sockaddr_in server_addr;//存储套接字信息的变量
     server_addr.sin_family = AF_INET;//指定了地址族为 AF_INET
     server_addr.sin_addr.s_addr = INADDR_ANY;//表示服务器将接受来自任何可用网络接口的连接请求
     server_addr.sin_port = htons(6666);//端口号不可以直接用数字赋值,htons将主机字节序(通常是小端字节序)的端口号转换为网络字节序(大端字节序)
 ​
     if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
         perror("server bind error:");
         return 0;
     }
 ​
     if (listen(server_socket, 10) < 0) {
         perror("server listen error:");
         return 0;
     }
     //printf("~~~~~~~~~~~~~\n");
     printf("TCP服务器准备完成,等待客户端的连接\n");
     //printf("~~~~~~~~~~~~~\n");
     int accept_socket;//创建一个存储接受到的客户端连接的套接字文件描述符。
     int res = 0;//后续用到
     char buffer[50] = { 0 };//定义缓冲区,用于暂时存储接收和发送的数据
     
     pthread_t thread_id;//线程编号
     while (1) //服务器将持续接收和发送数据,直到手动停止程序。
     {
         accept_socket = accept(server_socket, NULL, NULL);
         printf("有客户端连接到服务器!\n");
         //创建一个线程
         //第三个参数是执行线程的函数
         pthread_create(&thread_id, NULL, thread_fun, &accept_socket);
         //read函数就是接受客户端发来的数据,存储到buffer里面,返回值表示实际上从accept_socket那边读取到的字节数
         res = read(accept_socket, buffer, sizeof(buffer));
         printf("client read %s\n", buffer);
         //向accept_socket写入buffer中数据,写的数据的字节数为res
         write(accept_socket, buffer, res);
         memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
     }
 ​
     return 0;
 }

 

运行结果如下

补充内容:

遇到报错,address already in use

运行服务器代码

打开客户端1,输入"I am client 1"

打开客户端2,输入"I am client 2"

检查结果如下:

客户端1:

客户端2:

服务器:

服务器这样是正常现象