网站首页 > 技术文章 正文
文章目录
- 1.2 TCP并发服务器的意义
- 1.3 实现TCP并发服务器的方式
- 二、使用IO多路复用实现TCP并发服务器优势
- 四、TCP并发服务器的构建
- 4.2 填写服务器网络信息结构体
- 4.3 将服务器网络信息结构体与套接字绑定
- 4.4 将套接字设置为被动监听状态
- 4.5 创建文件描述符集合母本和子本并进行清空操作
- 4.6 将sockfd添加进入集合内,并更新最大文件描述符
- 4.7 循环实现内部功能伪代码
- 5.1步骤一和二和4.1,4.2一样
- 5.2 尝试与服务器建立连接
- 5.3 内部功能实现伪代码
- 七、TCP并发服务器源代码
一、服务器模型
1.1 服务器概念
服务器模型主要分为两种,循环服务器和并发服务器。
循环服务器:
在同一时间只能处理一个客户端的请求。
并发服务器:
在同一时间内能同时处理多个客户端的请求。
TCP的服务器默认的就是一个循环服务器,原因是有两个阻塞 accept函数 和recv函数 之间会相互影响。
UDP的服务器默认的就是一个并发服务器,因为只有一个阻塞的 recvfrom函数。
1.2 TCP并发服务器的意义
在有些应用场景下,我们既要 保证数据可靠 ,又要 支持并发
这就需要用到TCP并发服务器。
1.3 实现TCP并发服务器的方式
- 使用 多路IO复用 实现TCP并发服务器(常用)
- 使用 多进程 实现TCP并发服务器
- 使用 多线程 实现TCP并发服务器
本次我们学习第一个方式,使用 多路IO复用(select函数)实现TCP并发服务器 的实现,在后续博客中我们会依次讲解其他实现方式。
感兴趣可以收藏加关注哦。
二、使用IO多路复用实现TCP并发服务器优势
对于实际开发过程中:
如果使用多进程实现TCP并发服务器,并发量大的时候,对系统的资源占用量也会很大。
如果使用多线程,业务逻辑复杂的时候,又涉及到临近资源访问的问题
比较好的方式是使用多路IO复用实现TCP并发服务器。
三、select函数
功能:
实现IO多路复用
头文件:
#include <sys/select.h>
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数
@nfds:监视的最大文件描述符+1
@readfds:要监视的读文件描述符集合,如果不关心,可以传NULL
@writefds:要监视的写文件描述符集合,如果不关心,可以传NULL
@exceptfds:要监视的异常的文件描述符集合,如果不关心,可以传NULL
(一般我们只关心readfds)
@timeout:超时时间
为0时非阻塞
为NULL时永久阻塞
为结构体时阻塞一定时间
返回值:
成功 返回就绪文件描述符的个数
失败 返回-1,置位错误码
超时 返回0
void FD_CLR(int fd, fd_set *set);
功能:
删除集合中的文件描述符
参数:
@fd:文件描述符
@set:构建要监视的文件描述符集合
int FD_ISSET(int fd, fd_set *set);
功能:
判断文件描述符是否在集合中
参数:
@fd:文件描述符
@set:构建要监视的文件描述符集合
返回值:
为0时不在里面
非0时在里面
void FD_SET(int fd, fd_set *set);
功能:
将文件描述符添加到集合中
参数:
@fd:文件描述符
@set:构建要监视的文件描述符集合
void FD_ZERO(fd_set *set);
功能:
清空集合
参数:
@set:构建要监视的文件描述符集合
注意:
- select只能监视小于 FD_SETSIZE(1024) 的文件描述符。
- select函数在返回时会将没有就绪的文件描述符在表中擦除,
所以,在循环中调用select时,每次需要重新填充集合。
四、TCP并发服务器的构建
4.1 创建套接字
使用socket函数创建IPV4、TCP套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
4.2 填写服务器网络信息结构体
struct sockaddr_in serviceaddr;
memset(&serviceaddr, 0, sizeof(serviceaddr));
serviceaddr.sin_family = AF_INET;
serviceaddr.sin_addr.s_addr = inet_addr(argv[1]);
serviceaddr.sin_port = htons(atoi(argv[2]));
socklen_t serviceaddr_len = sizeof(serviceaddr);
4.3 将服务器网络信息结构体与套接字绑定
if (bind(sockfd, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
ERRLOG("bind error");
}
4.4 将套接字设置为被动监听状态
if (listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
4.5 创建文件描述符集合母本和子本并进行清空操作
fd_set readfds;
FD_ZERO(&readfds);
fd_set readfds_msg;
FD_ZERO(&readfds_msg);
4.6 将sockfd添加进入集合内,并更新最大文件描述符
FD_SET(sockfd, &readfds);
max_fd = max_fd > sockfd ? max_fd : sockfd;
4.7 循环实现内部功能伪代码
while(1){
select();
//遍历文件描述符集合
for(){
if(sockfd就绪了){
//说明有新的客户端建立连接了
acceptfd = accept();
将acceptfd加入到readfds中
更新最大文件描述符
}else{
//说明有客户端发来数据了
recv();
//如果recv返回0了 需要将当前的客户端的acceptfd在
//readfds中删除,后续就不再监视它了
strcat();
send();
}
}
}
五、客户端的构建
5.1步骤一和二和4.1,4.2一样
5.2 尝试与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("connect error");
}
5.3 内部功能实现伪代码
while(1){
//从终端获取数据写入buff中
fgets();
buff[strlen(buff)-1] = '\0';//清理结尾的\n
//发送数据
if(-1 == send(sockfd, buff, sizeof(buff), 0)){
ERRLOG("send error");
}
//接收服务器的应答信息
if(-1 == (nbytes = recv(sockfd, buff, sizeof(buff), 0))){
ERRLOG("recv error");
}
}
六、测试结果
使用三个客户端连接一个TCP并发服务器。
测试TCP并发服务器功能实现
测试quit退出功能实现
测试ctrl+c终止程序断开来连接实现
测试退出文件,清除客户端的文件描述符,在新的客户端连接时,从最小的文件描述符开始实现。
成功实现IO多路复用TCP并发服务器和客户端。
七、TCP并发服务器源代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/select.h>
#define ERRLOG(msg) \
do \
{
\
printf("%s %s %d:", __FILE__, __func__, __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]);
return -1;
}
//创建套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
ERRLOG("socket error");
}
//填写服务器网络信息结构体
struct sockaddr_in serviceaddr;
memset(&serviceaddr, 0, sizeof(serviceaddr));
serviceaddr.sin_family = AF_INET;
serviceaddr.sin_addr.s_addr = inet_addr(argv[1]);
serviceaddr.sin_port = htons(atoi(argv[2]));
socklen_t serviceaddr_len = sizeof(serviceaddr);
//将服务器网络信息结构体与套接字绑定
if (bind(sockfd, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
ERRLOG("bind error");
}
//将套接字设置为被动监听状态
if (listen(sockfd, 5) == -1)
{
ERRLOG("listen error");
}
char buf[N] = {
0};
int max_fd;
int i;
int ret;
int acceptfd;
int nbytes;
//创建文件描述符集合母本和子本并进行清空操作
fd_set readfds;
FD_ZERO(&readfds);
fd_set readfds_msg;
FD_ZERO(&readfds_msg);
//将sockfd添加进入集合内,并跟新最大文件描述符
FD_SET(sockfd, &readfds);
max_fd = max_fd > sockfd ? max_fd : sockfd;
while (1)
{
//在每次循环前将子本重新赋值,因为select会将没有就绪的文件描述符在集合内擦除
readfds_msg = readfds;
if ((ret = select(max_fd + 1, &readfds_msg, NULL, NULL, NULL)) == -1)
{
ERRLOG("select error");
}
else //说明有文件描述符就绪了
{
//遍历文件描述符
for (i = 3; i < max_fd + 1 && ret != 0; i++)
{
//判断是哪个文件描述符就绪了
if (FD_ISSET(i, &readfds_msg))
{
ret--;
if (i == sockfd) //如果套接字就绪了则等待客户端连接
{
if ((acceptfd = accept(sockfd, NULL, NULL)) == -1)
{
ERRLOG("accept error");
}
printf("客户端[%d]连接到服务器..\n", acceptfd);
//如果有客户端连接将产生的新的文件描述符添加到集合中,并更新最大文件描述符
FD_SET(acceptfd, &readfds);
max_fd = max_fd > acceptfd ? max_fd : acceptfd;
}
else //否则就是客户端发来消息了
{
memset(buf, 0, N);
if ((nbytes = recv(i, buf, N, 0)) == -1)
{
ERRLOG("recv error");
}
else if (nbytes == 0)
{
printf("客户端[%d]已断开连接..\n", i);
close(i); //关闭当前客户端的文件描述符
FD_CLR(i, &readfds); //将该客户端的文件描述符在集合中删除
continue;
}
if (strcmp(buf, "quit") == 0)
{
printf("客户端[%d]已退出服务器..\n", i);
close(i);
FD_CLR(i, &readfds);
continue;
}
printf("客户端[%d]发来消息[%s]..\n", i, buf);
strcat(buf, "--夜猫徐"); //组装应答
if (send(i, buf, N, 0) == -1) //发送给客户端
{
ERRLOG("send error");
}
}
}
}
}
}
close(sockfd);
return 0;
}
八、客户端源代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define ERRLOG(msg) do{
\
printf("%s %s %d:", __FILE__, __func__, __LINE__);\
perror(msg);\
exit(-1);\
}while(0)
#define N 128
int main(int argc, const char *argv[]){
//入参合理性检查
if(3 != argc){
printf("Usage : %s <IP> <PORT>\n", argv[0]);
return -1;
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//2.填充服务器网络信息结构体
struct sockaddr_in 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]);
socklen_t serveraddr_len = sizeof(serveraddr);
char buff[128] = {
0};
int nbytes = 0;
//3.尝试与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("connect error");
}
printf("与服务器建立连接成功..\n");
while(1){
memset(buff, 0, sizeof(buff));
fgets(buff, N, stdin);
buff[strlen(buff)-1] = '\0';//清理结尾的\n
//发送数据
if(-1 == send(sockfd, buff, sizeof(buff), 0)){
ERRLOG("send error");
}
//接收服务器的应答信息
if(-1 == (nbytes = recv(sockfd, buff, sizeof(buff), 0))){
ERRLOG("recv error");
}
if(0 == nbytes){
break;
}
//输出应答信息
printf("应答为:[%s]\n", buff);
}
//关闭套接字
close(sockfd);
return 0;
}
猜你喜欢
- 2024-11-03 linux 内核poll/select/epoll实现剖析(经典)-下
- 2024-11-03 Linux I/O复用中select poll epoll模型的介绍及其优缺点的比较
- 2024-11-03 IO多路复用之select、poll、epoll之间的区别总结
- 2024-11-03 网络编程——C++实现socket通信(TCP)高并发之select模式
- 2024-11-03 Java面试八股文Netty网络编程,select
- 2024-11-03 linux 内核poll/select/epoll实现剖析(经典)-上
- 2024-11-03 I/O复用 - select&poll(i/o复用不会阻塞用户进程)
- 2024-11-03 深入学习IO多路复用 select/poll/epoll 实现原理
- 2024-11-03 linux并发服务器模型三、Select(linux高并发服务器)
- 2024-11-03 趣谈网络协议栈(四),学习select和poll函数的内核实现
- 最近发表
-
- 使用Knative部署基于Spring Native的微服务
- 阿里p7大佬首次分享Spring Cloud学习笔记,带你从0搭建微服务
- ElasticSearch进阶篇之搞定在SpringBoot项目中的实战应用
- SpringCloud微服务架构实战:类目管理微服务开发
- SpringBoot+SpringCloud题目整理
- 《github精选系列》——SpringBoot 全家桶
- Springboot2.0学习2 超详细创建restful服务步骤
- SpringCloud系列:多模块聚合工程基本环境搭建「1」
- Spring Cloud Consul快速入门Demo
- Spring Cloud Contract快速入门Demo
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)