1、并发服务器
tcp通信目前只能实现一个服务器端对应一个客户端,不能实现一个服务器对应多个客户端,为了实现该操作需要引入并发操作 。
循环服务器:
cpp
#include <myhead.h>
#define SER_PORT 8888
#define SER_IP "192.168.xx.xx"
//服务器端口号
//服务器IP地址
int main()
{
//1、创建用于连接的套接字文件描述符
int sfd=socket(AF_INET,SOCK_STREAM,0);
//参数1:AF_INET表示使用的是ipv4的通信协议
//参数2:SOCK_STREAM表示使用的是tcp通信
//参数3:由于参数2指定了协议,参数3填0即可
if(sfd==-1)
{
perror("socket error");
return -1;
}
printf("socket success sfd=%d\n".sfd);
//2、绑定ip地址和端口号
//填充要绑定的IP地址和端口号结构体
struct socketaddr_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 绑定工作
//参数1:要被绑定的套接字文件描述符
//参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
//参数3:参数2的大小
if(bind(sfd, (structsockaddr*)&sin,sizeof(sin)) ==-1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
//3、启动监听
//参数1:要启动监听的文件描述符
//参数2:挂起队列长度
if(listen(sfd,128)==-1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
//4、阻塞等待客户端的连接请求
//定义变量,用于接受客户端地址信息结构体
struct sockaddr_in cin;//用于接收地址信息结构体的
socklen_t socklen=sizeof(cin); //用于接收地址信息的长度
while(1)
{
int newfd=accept(sfd,(struct sockaddr *)&cin &socklen);
//参数1:服务器套接字文件描述符
//参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
//参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]:已连接成功,newfd = %d!!!!\n", inet_ntoa(cin.sin_addr),
ntohs(cin.sin_port), newfd);
//5、数据收发
char rbuf[128]=""; //数据容器
while(1)
{
//清空容器的内容
bzero(rbuf,sizeof(rbuf));
//从套接字中读取消息
int res=recv(newfd,rbuf,sizeof(rbuf),0);
if(res==0)
{
printf("对端已经下线\n");
break;
}
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr),
ntohs(cin.sin_port), rbuf);
//对收到的数据处理一下,回给客户端
strcat(rbuf, "*_*");
//将消息发送给客户端
if(send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}
//6、关闭套接字
close(newfd);
}
close(sfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}
循环服务器的模型
cpp
sfd = socket(); //创建用于链接的套接字文件描述符
bind(); //绑定ip地址和端口号
listen(); //启动被动监听状态
while(1)
{
newfd = accept(); //阻塞接受客户端连接请求
//跟客户端进行数据收发
recv();
send();
close(newfd);
}
close(sfd);
accept函数和recv函数都是阻塞函数,但是,我们想让这两个阻塞任务同时执行,互不相干扰,此时,我们就要引入并发机制:多进程、多线程、IO多路复用。
2 多进程实现并发服务器
原理:主进程可以用于完成对客户端的连接请求,子进程可以完成对客户端通信操作
代码
cpp
#include <myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.xx.xx" //服务器IP地址
//自定义信号处理函数
void handler(int sign)
{
if(sign==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:AF_INET表示使用的是ipv4的通信协议
//参数2:SOCK_STREAM表示使用的是tcp通信
//参数3:由于参数2指定了协议,参数3填0即可
if(sfd==-1)
{
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", sfd); //3
//2、绑定ip地址和端口号
//2.1 填充要绑定的ip地址和端口号结构体
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 绑定工作
//参数1:要被绑定的套接字文件描述符
//参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
//参数3:参数2的大小
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) ==-1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
//3、启动监听
//参数1:要启动监听的文件描述符
//参数2:挂起队列的长度
if(listen(sfd, 128) ==-1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
//4、阻塞等待客户端的连接请求
//定义变量,用于接受客户端地址信息结构体
struct sockaddr_in cin; //用于接收地址信息结构体的
socklen_t socklen = sizeof(cin); //用于接收地址信息的长度
while(1)
{
int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
//参数1:服务器套接字文件描述符
//参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
//参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]:已连接成功,newfd = %d!!!!\n", inet_ntoa(cin.sin_addr),ntohs(cin.sin_port), newfd);
//创建子进程,用于跟客户端进行通信
pid_t pid = fork();
if(pid > 0)
{
close(newfd);
}else if(pid == 0)
{
//关闭sfd,因为子进程主要专注于跟客户端的通信,主要使用newfd
close(sfd);
//5、数据收发
char rbuf[128] = ""; //数据容器
while(1)
{
//清空容器中的内容
bzero(rbuf, sizeof(rbuf));
//从套接字中读取消息
int res = recv(newfd, rbuf, sizeof(rbuf), 0);
if(res == 0)
{
printf("对端已经下线\n");
break;
}
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr),
ntohs(cin.sin_port), rbuf);
//对收到的数据处理一下,回给客户端
strcat(rbuf, "*_*");
//将消息发送给客户端
if(send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}
//6、关闭套接字
close(newfd);
//退出子进程
exit(EXIT_SUCCESS);
}else
{
perror("fork error");
return -1;
}
}
close(sfd); //关闭监听
std::cout << "Hello, World!" << std::endl;
return 0;
}
3 多线程实现并发服务器
原理:主线程用于接收客户端的连接请求, 分支线程用于跟客户端进行通信
cpp
#include <myhead.h>
#define SER_PORT 8888 //服务器端口号
#define SER_IP "192.168.31.49" //服务器IP地址
//定义用于传送数据的结构体类型
struct Info
{
int newfd; //套接字文件描述符
struct sockaddr_in cin; //客户端套接字地址信息结构体
};
//分支线程,用户跟客户端进行通信
void *deal_cli_msg(void *arg)
{
//解析传过来的数据
int newfd = ((struct Info *)arg)->newfd; //解析套接字文件描述符
struct sockaddr_in cin = ((struct Info *)arg)->cin;
//5、数据收发
char rbuf[128] = ""; //数据容器
while(1)
{
//清空容器中的内容
bzero(rbuf, sizeof(rbuf));
//从套接字中读取消息
int res = recv(newfd, rbuf, sizeof(rbuf), 0);
if(res == 0)
{
printf("对端已经下线\n");
break;
}
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr),
ntohs(cin.sin_port), rbuf);
//对收到的数据处理一下,回给客户端
strcat(rbuf, "*_*");
//将消息发送给客户端
if(send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return NULL;
}
printf("发送成功\n");
}
//6、关闭套接字
close(newfd);
//退出分支线程
pthread_exit(NULL);
}
/******************************主程序********************************* */
int main(int argc, const char *argv[])
{
//1、创建用于连接的套接字文件描述符
int sfd = socket(AF_INET, SOCK_STREAM, 0);
//参数1:AF_INET表示使用的是ipv4的通信协议
//参数2:SOCK_STREAM表示使用的是tcp通信
//参数3:由于参数2指定了协议,参数3填0即可
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", sfd); //3
//2、绑定ip地址和端口号
//2.1 填充要绑定的ip地址和端口号结构体
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 绑定工作
//参数1:要被绑定的套接字文件描述符
//参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
//参数3:参数2的大小
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) ==-1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
//3、启动监听
//参数1:要启动监听的文件描述符
//参数2:挂起队列的长度
if(listen(sfd, 128) ==-1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
//4、阻塞等待客户端的连接请求
//定义变量,用于接受客户端地址信息结构体
struct sockaddr_in cin; //用于接收地址信息结构体的
socklen_t socklen = sizeof(cin); //用于接收地址信息的长度
while(1)
{
//对于accept函数而言,当程序执行到该函数时,会给当前客户端预分配一个最小未使用的文件
描述符
//后期再有新的小的文件描述符,也不使用了,新的最小的,用于下一次客户端的选定
int newfd = accept(sfd, (struct sockaddr *)&cin, &socklen);
//参数1:服务器套接字文件描述符
//参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
//参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]:已连接成功,newfd = %d!!!!\n", inet_ntoa(cin.sin_addr),
ntohs(cin.sin_port), newfd);
//填充要传输给分支线程的数据结构体变量
struct Info 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_join(tid, NULL); //? 不能使用该函数回收,因为该函数时阻塞函数
pthread_detach(tid); //将线程设置成分离态,执行结束后,系统自动回收资源
}
close(sfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}