前言
本文旨在介绍网络io的基本原理,与select模式是如何做到增加并发量的。
本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。
socket编程
socket介绍
传统的进程间通信借助内核提供的IPC机制进行, 但是只能限于本机通信, 若要跨机通信, 就必须使用网络通信( 本质上借助内核-内核提供了socket伪文件的机制实现通信----实际上是使用文件描述符), 这就需要用到内核提供给用户的socket API函数库。
使用socket会建立一个socket pair,如下图, 一个文件描述符操作两个缓冲区。
几乎所有的IO 接口 ( 包括socket 接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
服务器代码原型【一请求一响应模式】
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define MAXLNE 4096
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request========\n");
while (1) {
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
//close(connfd);
}
close(listenfd);
return 0;
}
问题
可以连接多个客户端,但是只有第一个连接的客户端可以传输数据
为什么后续的客户端可以连接?
因为listenfd在应用层启动的
-> 修改
使用while循环轮询listen代码
while(1){
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
}
-> 多客户端可连接,连接完成后只能发送一条数据
【fd阻塞】
-> 阻塞在accept之后,无法完成,无法接受新的数据
主要API函数介绍
socket
int socket(int domain, int type, int protocol);
函数描述: 创建socket
参数说明:
domain: 协议版本
- - AF_INET IPV4
- - AF_INET6 IPV6
- - AF_UNIX AF_LOCAL本地套接字使用
type:协议类型
- - SOCK_STREAM 流式, 默认使用的协议是TCP协议
- - SOCK_DGRAM 报式, 默认使用的是UDP协议
protocal:
- - 一般填0, 表示使用对应类型的默认协议.
返回值:
- - 成功: 返回一个大于0的文件描述符
- - 失败: 返回-1, 并设置errno
当调用socket函数以后, 返回一个文件描述符, 内核会提供与该文件描述符相对应的读和写缓冲区, 同时还有两个队列, 分别是请求连接队列和已连接队列(监听文件描述符才有,listenFd)
bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数描述: 将socket文件描述符和IP,PORT绑定
参数说明:
- socket: 调用socket函数返回的文件描述符
- addr: 本地服务器的IP地址和PORT
- addrlen: addr变量的占用的内存大小
返回值:
- 成功: 返回0
- 失败: 返回-1, 并设置errno
listen
int listen(int sockfd, int backlog);
函数描述: 将套接字由主动态变为被动态
参数说明:
- sockfd: 调用socket函数返回的文件描述符
- backlog: 在linux系统中,这里代表全连接队列(已连接队列)的数量。在unix系统种,这里代表全连接队列(已连接队列)+ 半连接队列(请求连接队列)的总数
返回值:
- 成功: 返回0
- 失败: 返回-1, 并设置errno
accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数说明:获得一个连接, 若当前没有连接则会阻塞等待.
函数参数:
- sockfd: 调用socket函数返回的文件描述符
- addr: 传出参数, 保存客户端的地址信息
- addrlen: 传入传出参数, addr变量所占内存空间大小
返回值:
- 成功: 返回一个新的文件描述符,用于和客户端通信
- 失败: 返回-1, 并设置errno值.
accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信. (内核会负责将请求队列中的连接拿到已连接队列中)
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数说明: 连接服务器
函数参数:
- sockfd: 调用socket函数返回的文件描述符
- addr: 服务端的地址信息
- addrlen: addr变量的内存大小
返回值:
- 成功: 返回0
- 失败: 返回-1, 并设置errno值
服务器模型-select
select介绍
多路IO技术: select, 同时监听多个文件描述符, 将监控的操作交给内核去处理
int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
数据类型fd_set::文件描述符集合——本质是位图
函数介绍: 委托内核监控该文件描述符对应的读,写或者错误事件的发生
参数说明:
- nfds: 最大的文件描述符+1
- readfds: 读集合, 是一个传入传出参数
传入: 指的是告诉内核哪些文件描述符需要监控
传出: 指的是内核告诉应用程序哪些文件描述符发生了变化
- writefds: 写文件描述符集合(传入传出参数,同上)
- execptfds: 异常文件描述符集合(传入传出参数,同上)
- timeout:
NULL--表示永久阻塞, 直到有事件发生
0 --表示不阻塞, 立刻返回, 不管是否有监控的事件发生
>0 --到指定事件或者有事件发生了就返回
- 返回值: 成功返回发生变化的文件描述符的个数。失败返回-1, 并设置errno值。
select-api
将fd从set集合中清除
void FD_CLR(int fd, fd_set *set);
功能描述: 判断fd是否在集合中
返回值: 如果fd在set集合中, 返回1, 否则返回0
int FD_ISSET(int fd, fd_set *set);
将fd设置到set集合中
void FD_SET(int fd, fd_set *set);
初始化set集合
void FD_ZERO(fd_set *set);
用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据,可写,错误发生
int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select优缺点
select优点:
- select支持跨平台
select缺点:
- 代码编写困难
- 会涉及到用户区到内核区的来回拷贝
- 当客户端多个连接, 但少数活跃的情况, select效率较低(例如: 作为极端的一种情况, 3-1023文件描述符全部打开, 但是只有1023有发送数据, select就显得效率低下)
- 最大支持1024个客户端连接(select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的, 而是由FD_SETSIZE=1024限制的)
FD_SETSIZE=1024 fd_set使用了该宏, 当然可以修改内核, 然后再重新编译内核, 一般不建议这么做。
select代码运行流程图
select代码实现
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define MAXLNE 4096
void *client_routine(void *arg){
//子线程只做 接收 和 发送
int connfd = *(int *)arg;
char buff[MAXLNE];
while (1) {
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
break;
}
}
return NULL;
}
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
#if 0
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request========\n");
while (1) {
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
//close(connfd);
}
# elif 0
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
}
# else
fd_set rfds,rset;
FD_ZERO(&rfds);
FD_SET(listenfd,&rfds);
int max_fd = listenfd;
while(1){
rset = rfds;
int nready = select(max_fd + 1, &rset , NULL , NULL , NULL);
if(FD_ISSET(listenfd,&rset)){
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd,&rfds);
if(connfd > max_fd) max_fd = connfd;
if(--nready == 0) continue;
}
int i = 0;
for(i = listenfd + 1;i <= max_fd;i ++){
if(FD_ISSET(i,&rset)){
n = recv(i, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(i, buff, n, 0);
} else if (n == 0) {
FD_CLR(i,&rfds);
close(i);
}
}
}
}
# endif
close(listenfd);
return 0;
}