常用的服务器模型

本文介绍了循环服务器(包括TCP和UDP)以及并发服务器的概念,提供了TCP多线程和多进程并发服务器的示例代码,并提及了IO多路复用服务器,特别是select方法在处理多个客户端连接中的应用。重点讨论了服务器模型的选择和实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、循环服务器

TCP循环服务器

UDP循环服务器

二、并发服务器

TCP多线程并发服务器

TCP多进程并发服务器

IO多路复用服务器


一、循环服务器

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个文件描述符。

文件描述符的特点:

  1. 文件描述符是非负整数
  2. 文件描述符一般从小到大分配的
  3. 进程启动的时候,默认分配三个文件描述符,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了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值