小目标3:如何实现多个客户端的连接
小目标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.
参数解释:
-
pthread_t *thread
:这是一个指向pthread_t
类型的指针,用于存储新线程的标识符。一旦线程创建成功,pthread_create
函数会将线程的标识符存储在这个指针所指向的内存中。 -
const pthread_attr_t *attr
:这是一个指向pthread_attr_t
类型的指针,用于指定线程的属性。通常情况下,可以将其设置为**NULL
,表示使用默认属性**。如果你需要指定线程的属性,可以使用pthread_attr_init
函数初始化一个线程属性对象,然后将其传递给pthread_create
。 -
void *(*start_routine) (void *)
:这是一个函数指针,指向一个函数,这个函数是新线程的入口点(也就是新线程要执行的函数)。这个函数必须接受一个void *
类型的参数,并返回一个void *
类型的指针。新线程将从这个函数开始执行。 -
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:
服务器: