一、服务器模型
在网络程序中往往一个服务器对多个客户机,为处理客户端请求,服务器有不同处理方法;
目前常用服务器模型:
循环服务器:循环服务器在同一时刻只能响应一个客户端,TCP默认为循环服务器,原因是accept与recv函数影响。
并发服务器:并发服务器在同一时刻可以响应多个客户端,UDP默认为并发服务器,原因是只有一个阻塞函数recvfrom。
二、如何实现TCP并发服务器
1.多进程实现TCP并发服务器;
2.多线程实现TCP并发服务器;
3.IO多路复用实现TCP并发服务器;
三种方式共用客户端:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#define ERR_LOG(msg) do{\
printf("%s %s %d\n",__func__,__FILE__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
#define N 128
int main(int argc,const char *argv[])
{
if(argc!=3){
printf("Usage:%s <IP> <PORT>\n",argv[0]);
exit(-1);
}
int sockfd=0;
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
ERR_LOG("socket error");
struct sockaddr_in serveraddr;
socklen_t serveraddr_len=sizeof(serveraddr);
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr=inet_addr(argv[1]);
if((connect(sockfd,(struct sockaddr *)&serveraddr,serveraddr_len))==-1)
ERR_LOG("connect error");
char buff[N]={0};
while(1){
memset(buff,0,N);
fgets(buff,N,stdin);
buff[strlen(buff)-1]='\0';
if((send(sockfd,buff,N,0))==-1)
ERR_LOG("send error");
if((recv(sockfd,buff,N,0))==-1)
ERR_LOG("recv error");
printf("%s\n",buff);
}
close(sockfd);
return 0;
}
1.1 多进程实现TCP并发服务器
主进程接收客户端请求,子进程负责收发数据。
服务器端代码:
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#define ERR_LOG(msg) \
do { \
printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
perror(msg); \
exit(-1); \
} while (0)
#define N 128
void sig_func(int sig)
{
if(sig == SIGCHLD)
{
wait(NULL);
}
}
int main(int argc, char const* argv[])
{
if (argc != 3) {
printf("Usage: %s <IP> <PORT>\n", argv[0]);
exit(-1);
}
// 1.创建套接字
int sockfd = 0;
int acceptfd = 0;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
ERR_LOG("socket error");
}
printf("sockfd = %d\n", sockfd);
// 2. 填充网络信息结构体
struct sockaddr_in serveraddr;
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
socklen_t serveraddr_len = sizeof(serveraddr);
memset(&serveraddr, 0, sizeof(serveraddr));
memset(&clientaddr, 0, sizeof(clientaddr));
serveraddr.sin_family = AF_INET;
// 使用命令行传参: ./server 192.168.1.106 8888
// 配置文件
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // 填服务器所在主机的IP地址192.168.250.100
serveraddr.sin_port = htons(atoi(argv[2]));
// 3. 绑定网络信息结构体
if (bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len) == -1) {
ERR_LOG("bind error");
}
// 4. 使套接字处于被动监听状态
if (listen(sockfd, 5) == -1) {
ERR_LOG("listen error");
}
int pid;
char buff[N] = { 0 };
int ret = 0;
printf("服务器正在运行...\n");
while (1) {
// 5. 阻塞等待连接
if ((acceptfd = accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len)) == -1) {
ERR_LOG("accept error");
}
printf("客户端[%s]:[%d]连接了...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
//------------多进程实现TCP并发服务器--------------
if ((pid = fork()) < 0) {
ERR_LOG("fork error");
} else if (pid > 0) {
// 父进程
close(acceptfd); //回收资源防止浪费
signal(SIGCHLD,sig_func);
} else {
// 子进程
close(sockfd); //回收资源防止浪费
while (1) {
memset(buff, 0, sizeof(buff));
if ((ret = recv(acceptfd, buff, N, 0)) == -1) {
ERR_LOG("recv error");
} else if (ret == 0) {
printf("客户端[%s]:[%d]退出...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
close(acceptfd);
break;
}
if (strcmp(buff, "quit") == 0) {
printf("客户端[%s]:[%d]退出...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
close(acceptfd);
break;
}
printf("buff=%s\n", buff);
strcat(buff, "^_^");
if (send(acceptfd, buff, N, 0) == -1) {
ERR_LOG("send error");
}
}
//退出子进程
exit(0);
}
}
close(acceptfd);
close(sockfd);
return 0;
}
多进程在服务器代码中需要注意一下几点:
1.父进程负责为子进程回收资源,但不能使用wait(默认阻塞),waitpid(需要在轮询中不断检测子进程退出),所以选择使用信号捕捉为子进程回收资源。
2.accept阻塞等待客户端来电,需要在循环中进行,不然只能接收一次客户端请求。
3.子进程退出必须使用exit(0);因为不使用exit子进程退出会进入循环再次执行循环代码。
1.2 多线程实现TCP并发服务器
主线程处理客户端的请求,子线程负责处理收发数据。
服务器端代码:
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define ERR_LOG(msg) \
do { \
printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
perror(msg); \
exit(-1); \
} while (0)
#define N 128
typedef struct {
int acceptfd;
struct sockaddr_in clientaddr;
} Msg_t;
// 线程处理函数
void* pthread_func(void* arg)
{
int ret = 0;
Msg_t msg = *((Msg_t*)arg);
char buff[N] = {0};
while (1) {
memset(buff, 0, sizeof(buff));
if ((ret = recv(msg.acceptfd, buff, N, 0)) == -1) {
printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
perror("recv error"); \
pthread_exit(NULL);
} else if (ret == 0) {
printf("客户端[%s]:[%d]退出...\n", inet_ntoa(msg.clientaddr.sin_addr), ntohs(msg.clientaddr.sin_port));
close(msg.acceptfd);
break;
}
if (strcmp(buff, "quit") == 0) {
printf("客户端[%s]:[%d]退出...\n", inet_ntoa(msg.clientaddr.sin_addr), ntohs(msg.clientaddr.sin_port));
close(msg.acceptfd);
break;
}
printf("buff=%s\n", buff);
strcat(buff, "^_^");
if (send(msg.acceptfd, buff, N, 0) == -1) {
printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
perror("send error"); \
pthread_exit(NULL);
}
}
pthread_exit(NULL);
}
int main(int argc, char const* argv[])
{
if (argc != 3) {
printf("Usage: %s <IP> <PORT>\n", argv[0]);
exit(-1);
}
// 1.创建套接字
int sockfd = 0;
int acceptfd = 0;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
ERR_LOG("socket error");
}
printf("sockfd = %d\n", sockfd);
// 2. 填充网络信息结构体
struct sockaddr_in serveraddr;
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
socklen_t serveraddr_len = sizeof(serveraddr);
memset(&serveraddr, 0, sizeof(serveraddr));
memset(&clientaddr, 0, sizeof(clientaddr));
serveraddr.sin_family = AF_INET;
// 使用命令行传参: ./server 192.168.1.106 8888
// 配置文件
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // 填服务器所在主机的IP地址192.168.250.100
serveraddr.sin_port = htons(atoi(argv[2]));
// 3. 绑定网络信息结构体
if (bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len) == -1) {
ERR_LOG("bind error");
}
// 4. 使套接字处于被动监听状态
if (listen(sockfd, 5) == -1) {
ERR_LOG("listen error");
}
int pid;
char buff[N] = { 0 };
int ret = 0;
pthread_t tid;
Msg_t msg;
printf("服务器正在运行...\n");
while (1) {
// 5. 阻塞等待连接
if ((acceptfd = accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len)) == -1) {
ERR_LOG("accept error");
}
printf("客户端[%s]:[%d]连接了...\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
msg.acceptfd = acceptfd;
msg.clientaddr = clientaddr;
//------------多线程实现TCP并发服务器--------------
pthread_create(&tid, NULL, pthread_func, (void*)&msg);
pthread_detach(tid);
}
close(acceptfd);
close(sockfd);
return 0;
}
多线程在服务器代码中需要注意一下几点:
1.在创建线程执行函数时,线程需要acceptfd和客户端网络信息结构体,所以必须传递结构体来实现。
2.第二个注意的点是必须Msg_t msg = *((Msg_t*)arg);来使用,不能定义指针msg_t* msg=(msg_t*)arg; 这种方法msg的网络信息结构体一直在变化,有其他客户端连接进来可能会导致前面的客户端不能使用。
3.第三点要将子线程设置为分离态,让系统回收子线程资源,主线程负责循环等待客户端请求。
4.在子线程中只能使用pthread_exit(NULL);退出。
1.3 IO多路复用实现TCP并发服务器
服务器代码:
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define ERR_LOG(msg) \
do { \
printf("%s %s %d\n", __func__, __FILE__, __LINE__); \
perror(msg); \
exit(-1); \
} while (0)
#define N 128
int main(int argc, char const* argv[])
{
if (argc != 3) {
printf("Usage: %s <IP> <PORT>\n", argv[0]);
exit(-1);
}
// 1.创建套接字
int sockfd = 0;
int acceptfd = 0;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
ERR_LOG("socket error");
}
// 2. 填充网络信息结构体
struct sockaddr_in serveraddr;
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
socklen_t serveraddr_len = sizeof(serveraddr);
memset(&serveraddr, 0, sizeof(serveraddr));
memset(&clientaddr, 0, sizeof(clientaddr));
serveraddr.sin_family = AF_INET;
// 使用命令行传参: ./server 192.168.1.106 8888
// 配置文件
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // 填服务器所在主机的IP地址192.168.250.100
serveraddr.sin_port = htons(atoi(argv[2]));
// 3. 绑定网络信息结构体
if (bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len) == -1) {
ERR_LOG("bind error");
}
// 4. 使套接字处于被动监听状态
if (listen(sockfd, 5) == -1) {
ERR_LOG("listen error");
}
printf("服务器正在运行...\n");
// -------------- 使用select实现TCP并发服务器---------
//---- 使用select 实现超时检测------
char buff[N] = {0};
int ret = 0;
//设置超时时间5s
struct timeval tm;
//1. 构建一张表
fd_set readfds;
fd_set readfds_temp;
int nfds = 0;
FD_ZERO(&readfds);
FD_ZERO(&readfds_temp);
//填充表
FD_SET(sockfd,&readfds);
nfds = sockfd > nfds ? sockfd:nfds;
while(1){
tm.tv_sec = 5;
tm.tv_usec = 0;
readfds_temp = readfds;
//使用select 阻塞等待文件描述符准备就绪
if((ret=select(nfds+1,&readfds_temp,NULL,NULL,&tm))==-1)
{
ERR_LOG("select error");
}else if (ret == 0)
{
printf("timeout...\n");
continue;
}
//select 就代表有文件描述符准备就绪
for(int i=3;i<nfds+1;i++)
{
if(FD_ISSET(i,&readfds_temp))
{
if(i == sockfd)
{
//有新的客户端连接
if((acceptfd = accept(sockfd,(struct sockaddr *)&clientaddr,&clientaddr_len))==-1)
{
ERR_LOG("accept error");
}
printf("[%d]连接了...\n",acceptfd);
//需要将acceptfd放入文件描述符的集合中
FD_SET(acceptfd,&readfds);
nfds = nfds > acceptfd? nfds:acceptfd;
}else{
//客户端发来数据
if((ret=recv(i,buff,sizeof(buff),0))==-1)
{
ERR_LOG("recv error");
}else if(ret == 0)
{
//对端关闭
printf("[%d]退出了...\n",i);
close(i);
FD_CLR(i,&readfds);
continue;
}
if(strcmp(buff,"quit")==0)
{
//对端关闭
printf("[%d]退出了...\n",i);
close(i);
FD_CLR(i,&readfds);
continue;
}
strcat(buff,"---hqyj");
if(send(i,buff,sizeof(buff),0)==-1)
{
ERR_LOG("send error");
}
}
}
}
}
close(acceptfd);
close(sockfd);
return 0;
}
IO多路复用在服务器代码中需要注意一下几点:
1.select 的返回值为已经准备好的客户端文件描述符,返回0表示在规定时间内无客户端装备好,延时结束。
2.整个代码逻辑为:当有客户端准备好,select监听到后,在for循环中依次遍历找到被置为1的那一个文件描述符,这个文件描述符有可能是sockfd,也有可能是acceptfd,所以进行if else 判断,如果是sockfd表示客户端第一次登录但没有发生信息,随后将acceptfd放入字符表中,当第二次客户端发送消息时,else代码负责进行数据的接收和发送。