对于之前的TCP服务器而言,一个服务器只能处理一个客户端你的操作,效率低,当客户端推出后,服务器也就结束了。
为了实现一个服务器可以应对多个客户端,我们先引入循环模型
一、循环TCP服务器
cs
#include<myhead.h>
#define SER_PORT 8888 //服务器端口号
#define CLI_IP "192.168.250.100" //服务器IP
int main(int argc, const char *argv[])
{
//1、创建一个套接字
int sfd = -1;
sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:表示创建的是网络通信的套接字
//参数2:表示使用的是TCP通信协议
//参数3:参数2指定了协议,参数3填0即可
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("%d success sfd = %d\n", __LINE__, sfd); //3
//2、绑定IP地址和端口号
//2.1填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; //地址族
sin.sin_port = htons(SER_PORT); //端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); //IP地址
//2.2 绑定
if(bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) ==-1)
{
perror("bind error");
return -1;
}
printf("%d bind success\n", __LINE__);
//3、将套接字设置成被动监听状态
if(listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("%d listen success\n", __LINE__);
//4、阻塞等待客户端的链接请求
int newfd = -1;
//定义结构体变量接收对方地址信息结构体
struct sockaddr_in cin; //用于接收客户端地址信息结构体
socklen_t addrlen = sizeof(cin); //用于接收客户端结构体的大小
while(1)
{
if((newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen)) == -1)
{
perror("accept error");
return -1;
}
printf("[%s %d]:发来连接请求\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
//5、收发数据
char rbuf[128] = ""; //用于接收客户发发来的数据
while(1)
{
//将容器清空
bzero(rbuf, sizeof(rbuf)); //memset(rbuf, 0, sizeof(rbuf));
//从套接字中读取数据'
int res = recv(newfd, rbuf, sizeof(rbuf)-1, 0);
if(res == 0)
{
printf("客户端已经下线\n");
break;
}
printf("[%s %d]: %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
//加个笑脸再回回去
strcat(rbuf,"*_*");
send(newfd, rbuf, strlen(rbuf), 0);
printf("发送成功\n");
}
//关闭跟客户端通信的套接字
close(newfd);
}
//6、关闭服务器
close(sfd);
return 0;
}
1.由于循环服务器每次只能处理一个客户端,下一个客户端的处理,必须建立在上一个客户端结束后
2.原因:接收函数是阻塞的,跟客户端交互时的读写函数也是阻塞的,想要多个阻塞任务并发执行,需要引入相关的机制完成
3.解决办法:多进程或多线程、IO多路复用
二、多进程实现TCP并发服务器
让父进程专门用于接收客户端的连接请求,每连接一个客户端,就创建一个子进程跟其进行通信,子进程负责通信
cs
#include <head.h>
#define SER_PORT 6666 // 服务器端口号
#define SER_IP "192.168.118.60" // 服务器IP
// 定义信号处理函数
void handler(int signo)
{
if (signo == SIGCHLD)
{
// 以非阻塞的形式回收僵尸进程
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
}
int main(int argc, const char *argv[])
{
// 将子进程退出时发射的SIGCHLD信号绑定到信号处理函数中
if (signal(SIGCHLD, handler) == SIG_ERR)
{
perror("signal");
return -1;
}
// 1、创建一个套接字
int sfd = -1;
sfd = socket(AF_INET, SOCK_STREAM, 0);
// 参数1:表示创建的是网络通信的套接字
// 参数2:表示使用的是TCP通信协议
// 参数3:参数2指定了协议,参数3填0即可
if (sfd == -1)
{
perror("socket error");
return -1;
}
printf("%d success sfd = %d\n", __LINE__, sfd); // 3
// 设置端口号快速重用
int reuse = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("端口号快速重用成功\n");
// 2、绑定IP地址和端口号
// 2.1填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; // 地址族
sin.sin_port = htons(SER_PORT); // 端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址
// 2.2 绑定
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("%d bind success\n", __LINE__);
// 3、将套接字设置成被动监听状态
if (listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("%d listen success\n", __LINE__);
// 4、阻塞等待客户端的链接请求
int newfd = -1;
// 定义结构体变量接收对方地址信息结构体
struct sockaddr_in cin; // 用于接收客户端地址信息结构体
socklen_t addrlen = sizeof(cin); // 用于接收客户端结构体的大小
// 定义进程号变量
pid_t pid = -1;
while (1)
{
// 父进程执行连续操作
if ((newfd = accept(sfd, (struct sockaddr *)&cin, &addrlen)) == -1)
{
perror("accept error");
return -1;
}
printf("[%s %d]:发来连接请求 newfd=%d\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
// 创建子进程
pid = fork();
if (pid > 0)
{
// 关闭newfd
close(newfd);
}
else if (pid == 0)
{
// 关闭sfd
close(sfd);
// 5、收发数据
char rbuf[128] = ""; // 用于接收客户发发来的数据
while (1)
{
// 将容器清空
bzero(rbuf, sizeof(rbuf)); // memset(rbuf, 0, sizeof(rbuf));
// 从套接字中读取数据'
int res = recv(newfd, rbuf, sizeof(rbuf) - 1, 0);
if (res == 0)
{
printf("客户端已经下线\n");
break;
}
printf("[%s %d]: %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
// 加个笑脸再回回去
strcat(rbuf, "*_*");
send(newfd, rbuf, strlen(rbuf), 0);
printf("发送成功\n");
}
// 关闭跟客户端通信的套接字
close(newfd);
// 退出子进程
exit(EXIT_SUCCESS);
}
}
// 关闭服务器
close(sfd);
return 0;
}
三、多线程实现并发服务器
1.原理:主线程用于接收客户端的连接请求,分支线程用于跟客户端进行交换
cs
#include <head.h>
#define SER_PORT 6666 // 服务器端口号
#define SER_IP "192.168.250.100" // 服务器IP
// 定义向线程提提供参数的结构体类型
struct BufInfo
{
int newfd; // 用于通信的客户端套接字
struct sockaddr_in cin; // 客户端地址信息结构体
};
// 定义线程体函数
void *deal_cli_msg(void *arg)
{
// 接收传过来的信息
int newfd = ((struct BufInfo *)arg)->newfd;
struct sockaddr_in cin = ((struct BufInfo *)arg)->cin;
// 5、收发数据
char rbuf[128] = ""; // 用于接收客户发发来的数据
while (1)
{
// 将容器清空
bzero(rbuf, sizeof(rbuf)); // memset(rbuf, 0, sizeof(rbuf));
// 从套接字中读取数据'
int res = recv(newfd, rbuf, sizeof(rbuf) - 1, 0);
if (res == 0)
{
printf("客户端已经下线\n");
break;
}
printf("[%s %d]: %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
// 加个笑脸再回回去
strcat(rbuf, "*_*");
send(newfd, rbuf, strlen(rbuf), 0);
printf("发送成功\n");
}
// 6、关闭服务器
close(newfd);
// 退出线程
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
// 1、创建一个套接字
int sfd = -1;
sfd = socket(AF_INET, SOCK_STREAM, 0);
// 参数1:表示创建的是网络通信的套接字
// 参数2:表示使用的是TCP通信协议
// 参数3:参数2指定了协议,参数3填0即可
if (sfd == -1)
{
perror("socket error");
return -1;
}
printf("%d success sfd = %d\n", __LINE__, sfd); // 3
// 设置端口号快速重用
int reuse = 1;
if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("端口号快速重用成功\n");
// 2、绑定IP地址和端口号
// 2.1填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; // 地址族
sin.sin_port = htons(SER_PORT); // 端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); // IP地址
// 2.2 绑定
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("%d bind success\n", __LINE__);
// 3、将套接字设置成被动监听状态
if (listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("%d listen success\n", __LINE__);
// 4、阻塞等待客户端的链接请求
int newfd = -1;
// 定义结构体变量接收对方地址信息结构体
struct sockaddr_in cin; // 用于接收客户端地址信息结构体
socklen_t addrlen = sizeof(cin); // 用于接收客户端结构体的大小
// 定义线程号变量用于接收线程
pthread_t tid = -1;
while (1)
{
// 当执行到accept函数时,该函数会预选一个当前最小的文件描述符
// 在预选后,到执行该函数期间,如果有更小的文件描述符被释放,也不会更新
// 直到下一次再运行到该函数时,会预选新的最小未使用的文件描述符
if ((newfd = accept(sfd, (struct sockaddr *)&cin, &addrlen)) == -1)
{
perror("accept error");
return -1;
}
printf("[%s %d]:发来连接请求\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
// 定义要传递数据的结构体变量
struct BufInfo buf;
buf.newfd = newfd;
buf.cin = cin;
// 创建分支线程用于处理客户端
if (pthread_create(&tid, NULL, deal_cli_msg, &buf) != 0)
{
fprintf(stderr, "pthread_create error\n");
return -1;
}
// 回收分支分支线程的资源
pthread_detach(tid);
}
close(sfd);
return 0;
}