一、引入
1> 对于UDP通信而言,由于是面向无连接的通信,一个服务器可以服务多个客户端,处理多个客户端的消息
2> 对于TCP服务器而言,需要建立连接,服务器域客户端建立连接后,其他客户端就无法通信了
3> 如何做到一个TCP服务器能够跟多个客户端进行通信,此时我们引入了TCP并发服务器
4> 实现TCP并发服务器可以使用:多进程、多线程、IO多路复用
二、循环服务器
2.1 循环服务器模型
1、sfd = socket(); //创建用于连接的套接字文件描述符
2、bind(); //为套接字绑定ip地址和端口号
3、listen(); //将套接字设置成被动监听状态
4、while(1)
{
newfd = accept(); //阻塞等待客户端的连接
recv();
send(); //数据收发
close(newfd);
}
close(sfd); //关闭套接字
2.2 循环服务器实现
#include<myhead.h>
#define SER_PORT 6666 //服务器端口号
#define SER_IP "192.168.0.136" //服务器ip地址
int main(int argc, const char *argv[])
{
//1、创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:表示ipv4的网络通信
//参数2:表示使用的是TCP通信方式
//参数3:表示默认使用一个协议
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success, sfd = %d\n", 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("bind success\n");
//3、将套接字设置成被动监听状态
if(listen(sfd, 128)==-1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
//4、阻塞等待客户端的连接请求
//4.1 定义变量用于接收客户端的信息
struct sockaddr_in cin; //用于接收地址信息
socklen_t addrlen = sizeof(cin); //用于接收长度
while(1)
{
//4.2 接收连接
int newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen) ;
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]: 已成功连接一个客户端\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
//5、数据收发
char buf[128] = "";
while(1)
{
//清空容器
bzero(buf, sizeof(buf));
//从客户端套接字中读取数据
int res = recv(newfd, buf, sizeof(buf), 0);
if(res == -1)
{
perror("read error");
return -1;
}else if(res == 0)
{
printf("客户端已经下线\n");
close(newfd); //关闭客户端套接字
break;
}
//正常收到客户端发来的消息
printf("[%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);
//对收到的消息进行一顿操作
//此处省略一万行代码
strcat(buf, "*_*");
//将消息回复给客户端
if(send(newfd, buf, strlen(buf), 0) == -1)
{
perror("发送error");
return -1;
}
printf("发送成功\n");
}
}
//6、关闭监听
close(sfd);
return 0;
}
三、使用多进程完成TCP并发服务器
3.1 实现模型
//定义收尸函数
void handler(int signo)
{
while(waitpid()>0);
}
signal(SIGCHLD, handler); //将信号与信号处理函数绑定
1、sfd = socket(); //创建用于连接的套接字文件描述符
2、bind(); //为套接字绑定ip地址和端口号
3、listen(); //将套接字设置成被动监听状态
4、while(1)
{
newfd = accept(); //阻塞等待客户端的连接
pid = fork(); //创建子进程用于处理客户端
if(pid > 0)
{
close(newfd); //关闭newfd
}else if(pid == 0)
{
close(sfd); //关闭sfd
recv();
send(); //数据收发
close(newfd);
}
}
close(sfd); //关闭套接字
3.2 实现代码
#include<myhead.h>
#define SER_PORT 6666 //服务器端口号
#define SER_IP "192.168.0.136" //服务器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 error");
return -1;
}
//1、创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:表示ipv4的网络通信
//参数2:表示使用的是TCP通信方式
//参数3:表示默认使用一个协议
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success, sfd = %d\n", 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("bind success\n");
//3、将套接字设置成被动监听状态
if(listen(sfd, 128)==-1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
//4、阻塞等待客户端的连接请求
//4.1 定义变量用于接收客户端的信息
struct sockaddr_in cin; //用于接收地址信息
socklen_t addrlen = sizeof(cin); //用于接收长度
while(1)
{
//4.2 接收连接
int newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen) ;
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d:%d]: 已成功连接一个客户端\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
pid_t pid = fork(); //创建一个子进程用于跟客户端通信
if(pid>0)
{
//关闭newfd
close(newfd);
}else if(pid == 0)
{
//关闭sfd
close(sfd);
//5、数据收发
char buf[128] = "";
while(1)
{
//清空容器
bzero(buf, sizeof(buf));
//从客户端套接字中读取数据
int res = recv(newfd, buf, sizeof(buf), 0);
if(res == -1)
{
perror("read error");
return -1;
}else if(res == 0)
{
printf("客户端已经下线\n");
close(newfd); //关闭客户端套接字
break;
}
//正常收到客户端发来的消息
printf("[%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);
//对收到的消息进行一顿操作
//此处省略一万行代码
strcat(buf, "*_*");
//将消息回复给客户端
if(send(newfd, buf, strlen(buf), 0) == -1)
{
perror("发送error");
return -1;
}
printf("发送成功\n");
}
//退出子进程
exit(EXIT_SUCCESS);
}
}
//6、关闭监听
close(sfd);
return 0;
}
四、使用多线程完成TCP并发服务器
实现原理:主线程主要完成accept接收客户端连接请求的阻塞,当客户端发来连接请求后,创建一个分支线程用于跟客户端进行通信
4.1 实现模型
4.2 实现代码
#include<myhead.h>
#define SER_PORT 6666 //服务器端口号
#define SER_IP "192.168.0.136" //服务器ip地址
//定义用于数据传递的结构体类型
struct MsgInfo
{
int newfd; //套接字文件描述符
struct sockaddr_in cin; //客户端地址信息结构体
};
//定义线程体函数
void *deal_cli_msg(void *arg)
{
//解析主线程传过来的数据
int newfd = (*(struct MsgInfo*)arg).newfd;
struct sockaddr_in cin = (*(struct MsgInfo*)arg).cin;
//5、数据收发
char buf[128] = "";
while(1)
{
//清空容器
bzero(buf, sizeof(buf));
//从客户端套接字中读取数据
int res = recv(newfd, buf, sizeof(buf), 0);
if(res == -1)
{
perror("read error");
return NULL;
}else if(res == 0)
{
printf("客户端已经下线\n");
close(newfd); //关闭客户端套接字
break;
}
//正常收到客户端发来的消息
printf("[%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);
//对收到的消息进行一顿操作
//此处省略一万行代码
strcat(buf, "*_*");
//将消息回复给客户端
if(send(newfd, buf, strlen(buf), 0) == -1)
{
perror("发送error");
return NULL;
}
printf("发送成功\n");
}
//退出线程
pthread_exit(NULL);
}
/******************************主程序*****************************/
int main(int argc, const char *argv[])
{
//1、创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:表示ipv4的网络通信
//参数2:表示使用的是TCP通信方式
//参数3:表示默认使用一个协议
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success, sfd = %d\n", 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("bind success\n");
//3、将套接字设置成被动监听状态
if(listen(sfd, 128)==-1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
//4、阻塞等待客户端的连接请求
//4.1 定义变量用于接收客户端的信息
struct sockaddr_in cin; //用于接收地址信息
socklen_t addrlen = sizeof(cin); //用于接收长度
while(1)
{
//4.2 接收连接
//accept在阻塞接收客户端连接请求时,会给当前的客户端预选一个最小的未分配的套接字文件描述符
//如果后期有更小的产生,也不会更新当前文件描述符了
//更小的文件描述符,用于后面的客户端连接使用
int newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen) ;
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d:%d]: 已成功连接一个客户端\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
//定义结构体变量,用于数据传递给线程体
struct MsgInfo buf = {newfd, cin};
//创建用于通信的分支线程
pthread_t tid = -1;
if(pthread_create(&tid, NULL, deal_cli_msg, &buf) != 0)
{
printf("pthread_create error\n");
return -1;
}
//主线程用于给分支线程收尸
pthread_detach(tid); //将线程设置成分离态
}
//6、关闭监听
close(sfd);
return 0;
}