目录
一、循环服务器
TCP循环服务器
服务器一次只能接入一个客户端,处理一个客户端的消息,该客户端如果不退出,其他客户端则无法接入。(所以这个不常用)
伪代码
listenFd=socket();
bind(listenFd,......);
listen(listenFd,.....);
while(1)
{
connFd=accept(listenFd,......);
while(1)
{
recv(connFd,.....);
process();
send(connFd,.....);
}
}
UDP循环服务器
只要处理每个客户端消息的时间不长,服务器可以同时处理多个客户端的请求
UDP循环服务器可以满足多个应用场景。
伪代码
sockFd=socket();
bind(sockFd);
while(1)
{
recvfrom(sockFd,.......);
process();
sendto(sockFd,..........);
}
二、并发服务器
TCP多线程并发服务器
客户端接入服务器后,服务器为客户端分配单独的线程,为该客户端提供服务。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERV_PORT 5003
void *client_handler(void *arg)
{
int connFd = *((int *)arg);
// 5.收消息
int ret;
char buf[BUFSIZ];
while (1) {
do {
memset(buf, 0, sizeof(buf));
ret = recv(connFd, buf, sizeof(buf) - 1, 0);
} while (ret < 0 && errno == EINTR);
// 接收消息出错
if (ret < 0) {
perror("recv error");
continue;
}
// 客户端关闭套接字
if (ret == 0) {
printf("client exit\n");
break;
}
printf("recv:%s", buf);
}
// 关闭套接字
close(connFd);
return NULL;
}
int main()
{
// 1.创建套接字
int listenFd = socket(AF_INET, SOCK_STREAM, 0);
if (listenFd < 0) {
perror("socket error");
exit(1);
}
printf("socket ok\n");
// 允许本地地址和端口号快速重用
int on = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
// 2.绑定IP地址和端口号
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(listenFd, (struct sockaddr *)&sin, sizeof(sin));
if (ret < 0) {
perror("bind error");
exit(1);
}
printf("bind ok\n");
// 3.设置监听套接字
ret = listen(listenFd, 5);
if (ret < 0) {
perror("listen error");
exit(1);
}
printf("listen ok\n");
int connFd;
struct sockaddr_in cin;
socklen_t len;
pthread_t tid;
while (1) {
// 4.接受客户端的连接
memset(&cin, 0, sizeof(cin));
len = sizeof(cin);
connFd = accept(listenFd, (struct sockaddr *)&cin, &len);
if (connFd < 0) {
perror("accept error");
continue;
}
printf("client(%s:%d) connect ok\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
pthread_create(&tid, NULL, client_handler, &connFd);
pthread_detach(tid);
}
// 6.关闭套接字
close(listenFd);
return 0;
}
TCP多进程并发服务器
客户端接入服务器后,服务器为客户端分配单独的进程,为该客户端提供服务。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#define SERV_PORT 5004
void client_handler(int connFd)
{
// 接收消息
char buf[BUFSIZ];
int ret;
while (1) {
do {
memset(buf, 0, sizeof(buf));
ret = recv(connFd, buf, sizeof(buf) - 1, 0);
} while (ret < 0 && errno == EINTR);
// 接收消息出错
if (ret < 0) {
perror("recv error");
continue;
}
// 客户端关闭套接字
if (ret == 0) {
printf("client exit\n");
break;
}
printf("recv:%s", buf);
}
// 关闭套接字
close(connFd);
}
void sig_handler(int signo)
{
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid > 0) {
printf("child pid=%d exit\n", pid);
} else {
printf("pid=%d\n", pid);
}
}
int main()
{
// 回收子进程资源
signal(SIGCHLD, sig_handler);
// 1.创建套接字
int listenFd = socket(AF_INET, SOCK_STREAM, 0);
if (listenFd < 0) {
perror("socket error");
exit(1);
}
printf("socket ok\n");
// 允许本地地址和端口号快速重用
int on = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
// 2.绑定IP地址和端口号
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(listenFd, (struct sockaddr *)&sin, sizeof(sin));
if (ret < 0) {
perror("bind error");
exit(1);
}
printf("bind ok\n");
// 3.设置监听套接字
ret = listen(listenFd, 5);
if (ret < 0) {
perror("listen error");
exit(1);
}
printf("listen ok\n");
int connFd;
struct sockaddr_in cin;
socklen_t len;
pid_t pid;
while (1) {
// 4.接受客户端的连接
memset(&cin, 0, sizeof(cin));
len = sizeof(cin);
connFd = accept(listenFd, (struct sockaddr *)&cin, &len);
if (connFd < 0) {
perror("accept error");
continue;
}
printf("client(%s:%d) connect ok\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
// 创建子进程为客户端提供服务
pid = fork();
if (pid < 0) {
perror("fork error");
continue;
}
if (pid == 0) { // pid等于0是子进程
close(listenFd);
client_handler(connFd);
exit(0);
} else { // pid大于0是父进程
close(connFd);
}
}
// 关闭套接字
close(listenFd);
return 0;
}
IO多路复用服务器
在服务器对每个客户端消息处理时间都不长的情况下,可以考虑使用IO多路复用服务器
Linux默认情况下最多能打开1024个文件,对应1024个文件描述符。
文件描述符的特点:
- 文件描述符是非负整数
- 文件描述符一般从小到大分配的
- 进程启动的时候,默认分配三个文件描述符,0、1、2
IO多路复用不只针对套接字文件描述符,也针对普通的文件描述符。
IO多路复用的3种实现方式:select、poll、epoll
下面以select进行举例
int select(int maxfd, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);
select宏的形式:
void FD_ZERO(fd_set *fdset) 清空文件描述符集合
void FD_SET(int fd,fd_set *fdset) 将fd加入文件描述符集合
void FD_CLR(int fd,fd_set *fdset) 将fd从文件描述符清空
int FD_ISSET(int fd,fd_set *fdset) 判断fd是否在文件描述符集合里其实
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SERV_PORT 5001
#define QUIT_STR "quit"
int main()
{
//创建套接字
int listenFd=socket(AF_INET,SOCK_STREAM,0);
if(listenFd<0)
{
perror("socket error");
exit(1);
}
printf("socket ok\n");
//允许地址和端口号快速重用
int on=1;
setsockopt(listenFd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
//绑定ip地址和端口号
struct sockaddr_in sin;
memset(&sin,0,sizeof(sin));
sin.sin_family=AF_INET;
sin.sin_port=htons(SERV_PORT);
sin.sin_addr.s_addr=htonl(INADDR_ANY);
int ret=bind(listenFd,(struct sockaddr *)&sin,sizeof(sin));
if(ret<0)
{
perror("bind error");
exit(1);
}
printf("bind ok\n");
//设置监听套接字
ret=listen(listenFd,5);
if(ret<0)
{
perror("listen error");
exit(1);
}
printf("listen ok\n");
//等待客户端的连接
fd_set rset;
fd_set tset;
FD_ZERO(&rset);
FD_SET(0,&rset);
FD_SET(listenFd,&rset);
int maxFd=listenFd;
char buf[BUFSIZ];
int connFd;
struct sockaddr_in cin;
socklen_t len;
while(1)
{
tset=rset;
ret=select(maxFd+1,&tset,NULL,NULL,NULL);
if(ret<0)
{
perror("select error");
continue;
}
for(int i=0;i<maxFd+1;i++)
{
//文件描述符对应的io通道没有数据发生
if(FD_ISSET(i,&tset)==0)
{
continue;
}
//标准输入上有数据发生
if(i==0)
{
memset(buf,0,sizeof(buf));
if(fgets(buf,sizeof(buf),stdin)==NULL)
{
perror("fgets error");
continue;
}
//输入quit,服务器退出
if (strncasecmp(buf,QUIT_STR,strlen(QUIT_STR))==0)
{
printf("input quit,server exit\n");
exit(0);
}
}
else if(i==listenFd)
{
//有客户端接入
//接受客户端的连接
memset(&cin,0,sizeof(cin));
len=sizeof(cin);
connFd=accept(listenFd,(struct sockaddr *)&cin,&len);
if(connFd<0)
{
perror("accept error");
continue;
}
printf("client(%s:%d) connect ok\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
//更新maxfd
if(connFd>maxFd)
{
maxFd=connFd;
}
FD_SET(connFd,&rset);
}
else
{
do
{
memset(buf,0,sizeof(buf));
ret=recv(i,buf,sizeof(buf)-1,0);
}
while(ret<0&&errno==EINTR);
//接收消息出错
if(ret<0)
{
perror("recv error");
continue;
}
//客户关闭套接字
if(ret==0)
{
FD_CLR(i,&rset);
close(i);
continue;
}
printf("recv:%s",buf);
}
}
}
//关闭套接字
close(listenFd);
return 0;
}
以上服务器模型中,最常用的就是IO多路复用服务器,如果精力有限,只需要搞懂这种就OK了。