小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
一、服务器模型
1.1 相关概念
服务器模型主要分为循环服务器、并发服务器
-
循环服务器:
循环服务器在同一个时刻只能响应一个客户端的请求
-
并发服务器:
并发服务器在同一个时刻可以响应多个客户端的请求
==TCP服务器默认是循环服务器==,因为TCP服务器有两个读阻塞函数accept和recv,这两个函数默认无法独立执行,所以无法实现并发
==UDP服务器默认是并发服务器==,因为UDP服务器只有一个读阻塞函数recvfrom
1.2 如何实现TCP并发服务器
- 方法1:使用多线程实现TCP并发服务器
- 方法2:使用多进程实现TCP并发服务器
- 方法3:使用IO多路复用实现TCP并发服务器
1.3 使用多线程实现TCP并发服务器
主控线程执行accept负责连接,只要有客户端跟服务器连接,就立即创建
一个子线程专门与指定客户端通信
PthreadFun:
while(1)
{
recv()/send()
}
main:
socket()
struct sockaddr_in serveraddr
bind()
listen()
while(1)
{
accept()
pthread_create(PthreadFun)
pthread_detach()
}
1.3.1 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <pthread.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
typedef struct{
int acceptfd;
struct sockaddr_in clientaddr;
}MSG;
void *PthreadFun(void *arg)
{
char buf[N] = {0};
ssize_t bytes;
MSG msg = *(MSG *)arg;
while(1)
{
if((bytes = recv(msg.acceptfd, buf, N, 0)) == -1)
{
ERRLOG("recv error");
}
else if(bytes == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(msg.clientaddr.sin_addr), ntohs(msg.clientaddr.sin_port));
pthread_exit(NULL);
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(msg.clientaddr.sin_addr), ntohs(msg.clientaddr.sin_port));
pthread_exit(NULL);
}
printf("%s-%d:%s\n", inet_ntoa(msg.clientaddr.sin_addr), ntohs(msg.clientaddr.sin_port), buf);
strcat(buf, "^_^");
if(send(msg.acceptfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
}
}
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用多线程实现TCP并发服务器
//主控线程执行accept负责连接,如果有客户端连接,就
//创建一个子线程与之通信
pthread_t thread;
MSG msg;
while(1)
{
//第五步:阻塞等待客户端的连接
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
ERRLOG("accept error");
}
//打印客户端的信息
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
//创建子线程
msg.acceptfd = acceptfd;
msg.clientaddr = clientaddr;
if(pthread_create(&thread, NULL, PthreadFun, &msg) != 0)
{
ERRLOG("pthread_create error");
}
//设置分离态
pthread_detach(thread);
}
return 0;
}
1.3.2 客户端
//TCP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:给服务器发送客户端的连接请求
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("connect error");
}
//进行通信
char buf[N] = {0};
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(send(sockfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端退出了\n");
exit(0);
}
memset(buf, 0, N);
if(recv(sockfd, buf, N, 0) == -1)
{
ERRLOG("recv error");
}
printf("服务器:%s\n", buf);
}
return 0;
}
1.4 使用多进程实现TCP并发服务器
父进程执行accept连接,如果有客户端连接,则创建子进程与之通信
main:
socket()
struct sockaddr_in serveraddr
bind()
listen()
while(1)
{
accept()
pid = fork()
if(pid > 0)
{
}
else if(pid == 0)
{
while(1)
{
recv()/send()
}
}
}
1.4.1 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
void handler(int sig)
{
waitpid(-1, NULL, WNOHANG);
}
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
ssize_t bytes;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
//注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
//如果是自己主机的客户端服务器测试,可以使用127网段的
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用多进程实现TCP并发服务器
pid_t pid;
//当客户端退出之后,与之对应的子进程就要退出,但是父进程没有结束,
//此时的子进程会变成僵尸进程,没有释放资源
//处理僵尸进程的方法:
//方法1:退出父进程,但是一般服务器不会结束
//方法2:使用wait函数,但是他是一个阻塞函数,父进程调用的话就无法执行accept了
//方法3:使用waitpid函数设置非阻塞,但是无法实时处理
//方法4:使用信号,这是最好的方法,异步通信可以实时处理僵尸进程
//signal(SIGCHLD, handler);
signal(SIGUSR1, handler);
while(1)
{
//第五步:阻塞等待客户端的连接
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
ERRLOG("accept error");
}
//打印客户端的信息
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
if((pid = fork()) == -1)
{
ERRLOG("fork error");
}
else if(pid > 0) //父进程负责连接
{
//wait(NULL);
//waitpid(-1, NULL, WNOHANG);
}
else //子进程负责通信
{
while(1)
{
if((bytes = recv(acceptfd, buf, N, 0)) == -1)
{
ERRLOG("recv error");
}
else if(bytes == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
kill(getppid(), SIGUSR1);
exit(1);
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
kill(getppid(), SIGUSR1);
exit(1);
}
printf("%s-%d:%s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), buf);
strcat(buf, "^_^");
if(send(acceptfd, buf, N, 0) == -1)
{
ERRLOG("send error");
}
}
}
}
return 0;
}
1.4.2 客户端
客户端同多线程客户端
1.5 使用IO多路复用实现TCP并发服务器(select)
因为TCP服务器端有多个阻塞函数,无法同时运行,所以使用IO多路复用解决这个问题
1.5.1 select
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:允许一个程序操作多个文件描述符,阻塞等待文件描述符
准备就绪,如果有文件描述符准备就绪,函数立即返回,
执行相应的IO操作
参数:
nfds:最大的文件描述符加1
readfds:保存读操作文件描述符的集合
writefds:保存写操作文件描述符的集合
exceptfds:保存其他或者异常的文件描述符的集合
timeout:超时
struct timeval {
long tv_sec; 秒
long tv_usec; 微秒
};
参数设置为NULL 阻塞
结构体中的成员设置为0 非阻塞
返回值:
成功:准备就绪的文件描述符的个数
失败:-1
清空集合set
void FD_ZERO(fd_set *set);
将文件描述符fd添加到集合set中
void FD_SET(int fd, fd_set *set);
将文件描述符fd从集合set中移除
void FD_CLR(int fd, fd_set *set);
判断文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set);
返回值:
存在:1
不存在:0
main:
socket()
struct sockaddr_in serveraddr
bind()
listen()
while(1)
{
select(maxfd, &readfds)
for(i = 0; i <= maxfd; i++)
{
if(FD_ISSET(i, &readfds) == 1)
{
if(i == sockfd)
{
accept(i/sockfd)
}
else
{
recv(i)/send(i)
}
}
}
}
1.5.2 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
ssize_t bytes;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
//atoi:将数字型字符串转换为整形数据
//htons:将主机字节序转化为网络字节序
serveraddr.sin_family = AF_INET;
//注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
//如果是自己主机的客户端服务器测试,可以使用127网段的
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
//使用IO多路服用的方法实现TCP并发服务器
int i;
//创建保存要操作的文件描述符的集合并清空
fd_set readfds, tempfds;
FD_ZERO(&readfds);
int maxfd = sockfd;
//将要操作的文件描述符添加到集合中
FD_SET(sockfd, &tempfds);
while(1)
{
readfds = tempfds;
//调用select函数阻塞等待文件描述符准备就绪
if(select(maxfd+1, &readfds, NULL, NULL, NULL) == -1)
{
ERRLOG("select error");
}
//如果有文件描述符准备就绪,select函数立即返回,判断哪一个文件
//描述符还在集合中,在的就是准备就绪的
//由于只要客户端连接服务器,服务器端accept就会产生文件描述符,
//所以加入到集合中的文件描述符有很多个,所以需要通过循环判断哪个
//文件描述符还在集合中
for(i = 0; i <= maxfd; i++)
{
if(FD_ISSET(i, &readfds) == 1)
{
if(i == sockfd)
{
//第五步:阻塞等待客户端的连接
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
ERRLOG("accept error");
}
//打印客户端的信息
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
//将新产生且要操作的文件描述符加入到集合中
FD_SET(acceptfd, &tempfds);
//设置最大的文件描述符
maxfd = (maxfd > acceptfd ? maxfd : acceptfd);
}
else
{
if((bytes = recv(i, buf, N, 0)) == -1)
{
ERRLOG("recv error");
}
else if(bytes == 0)
{
printf("客户端%d退出了\n", i);
FD_CLR(i, &tempfds);
close(i);
break;
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端%d退出了\n", i);
FD_CLR(i, &tempfds);
close(i);
break;
}
printf("客户端%d:%s\n", i, buf);
strcat(buf, "^_^");
if(send(i, buf, N, 0) == -1)
{
ERRLOG("send error");
}
}
}
}
}
return 0;
}
1.5.3 客户端
客户端同多线程客户端
1.6 使用IO多路复用实现TCP并发服务器(poll)
1.6.1 poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:同select,阻塞等待文件描述符准备就绪,如果有文件描述符
准备就绪,则函数立即返回并执行相应的IO操作
参数:
fds:结构体数组,有多少个元素由要操作的文件描述符的个数决定
struct pollfd {
int fd; 文件描述符
short events; 请求的事件
POLLIN 有数据可读
short revents; 返回的事件
};
nfds:要操作的文件描述符的个数
timeout:超时检测
>0 设置超时毫秒数
0 非阻塞
<0 阻塞
返回值:
成功:准备就绪的文件描述符的个数
失败:-1
1.6.2 服务器
//TCP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/stat.h>
#include <fcntl.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s-%d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
char buf[N] = {0};
//创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//填充服务器网络信息结构体
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据
//inet_addr:将点分十进制ip地址转化为32位整形数据
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//将套接字设置为被动监听状态
if(listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
mkfifo("myfifo", 0664);
int fifofd;
if((fifofd = open("myfifo", O_RDWR)) == -1)
{
ERRLOG("open error");
}
//使用poll函数实现IO多路复用
//第一步:定义结构体数组
//结构体数组元素的个数由操作的文件描述符的个数决定
struct pollfd myfd[3];
//第二步:给结构体数组的每一个元素的成员赋值
myfd[0].fd = 0;
myfd[0].events = POLLIN; //将请求的事件设置为有数据可读
myfd[1].fd = sockfd;
myfd[1].events = POLLIN;
myfd[2].fd = fifofd;
myfd[2].events = POLLIN;
int nfds = 3;
while(1)
{
//使用poll函数阻塞等待文件描述符准备就绪
if (poll(myfd, nfds, -1) == -1)
{
ERRLOG("poll error");
}
//如果有文件描述符准备就绪,则poll函数立即返回
//判断每一个文件描述符对应的返回的事件是否跟请求的事件的标志位一致
if(myfd[0].revents & POLLIN == 1)
{
if(fgets(buf, N, stdin) == NULL)
{
ERRLOG("fgets error");
}
buf[strlen(buf) - 1] = '\0';
printf("buf = %s\n", buf);
}
if(myfd[1].revents & POLLIN == 1)
{
if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("accept error");
}
printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
}
if(myfd[2].revents & POLLIN == 1)
{
if(read(fifofd, buf, 128) == -1)
{
ERRLOG("read error");
}
printf("fifo: %s\n", buf);
}
}
return 0;
}
1.6.3 客户端
客户端同多线程客户端