前言
最近一段时间在研究redis,才发现自己基础不牢靠,所以最近一直在学习网络原理及c语言socket方面的知识,在此推荐前一段时间写的网络原理,虽然不太好,但是作为网络原理入个门是绰绰有余了,redis每秒的读写近十万,而且redis还是单线程的,这为它不需要考虑线程安全问题省了不少事,redis之所以这么快,就是因为他的io模型很优秀。
io从最开始的bio,发展到nio,aio再到优秀的网络通信框架mina,netty,这些都离不开io模型从select到poll再到epoll的一步步优化。
为了学习select,笔者也找了不少资料,视频,博客都找了,it圈懒人太多,博客大多复制粘贴,盗图盗文。然而我觉得还是有相当一部分的大神,只是我没找到他们的博客或是没有写博客的习惯。我之所以坚持写博客主要是为了自己学的东西有反馈,其次是为了大家方便。好了,下面进入正文
ps:看本文前最好先去研究笔者其他博文,对理解本文有帮助
借鉴bio不足研究nio
网络原理
c实现bio socket
c实现 多进程服务器
c实现多线程服务器
select模型
看过太多说select模型的,大多数是把unix几张io模型图放上来,再有就是搬概念,其实他们自己根本就不懂select模型,也鲜有人去写c语言的基于select 的scoket,特别是对于我们java程序员来说,研究c语言的有几个。下面我介绍select模型不会搬概念,我只说一些自己的理解,然后结合例子深度一行一行代码剖析他。在此推荐我之前的一篇博文单线程并发服务器,对于理解单线程实现并发服务器有很大帮助,和select的原理有点类似,只是层次不一样,但对于理解select有一定帮助
select 是linux内核提供给我们的一个函数,他可以做到的事情有当有读写事件发生的时候,会告诉我们有多少个事件就绪,但是他不会告诉我们具体是哪些事件就绪,需要我们自己去事件集一个一个遍历判断。你可能看不懂上面的描述,没关系,只要你自己把我的例子敲一遍应该就懂一半了。
select例子
select服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 4096
#define SERV_PORT 9999
int main(int argc, char *argv[])
{
//文件描述符:标准输入是0,标准输出是1,标准错误是2
// maxi:最大文件描述符的下标
// maxfd:最大文件描述符
//listenfd:建立socket生成的监听作用的文件描述符
// connfd:客户端连接后返回的文件描述符 ,可以用这个文件描述符和客户端通信
//sockfd :是 client数组保存的 connfd
int i, maxi, maxfd, listenfd, connfd, sockfd;
//nready:就绪事件个数,client数组保存 所有客户端连接后返回的connfd ,FD_SETSIZE=1024
int nready, client[FD_SETSIZE];
//read 返回
ssize_t n;
//读集合,
fd_set rset, allset;
// 读写缓冲区
char buf[MAXLINE];
//客户端ip
char cli_ip[INET_ADDRSTRLEN];
//客户端地址长度
socklen_t cliaddr_len;
//客户端地址,服务器地址
struct sockaddr_in cliaddr, servaddr;
//服务器建立socket
listenfd = socket(AF_INET, SOCK_STREAM, 0);
printf("listenfd:%d\n",listenfd);//listenfd=3
//清零
bzero(&servaddr, sizeof(servaddr));
//ipv4
servaddr.sin_family= AF_INET;
//ip
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//port
servaddr.sin_port= htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
//设置同时能够接受128个请求,注意是同一时间
listen(listenfd, 128);
printf("wait for connect...\n");
//最大fd=3
maxfd = listenfd;
//下标暂时没有,因为到这里还没有客户端连接
maxi = -1;
//初始化client数组 全部-1,因为到这里没有客户端连接
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1;
//清空位图
FD_ZERO(&allset);
//位图操作 数组元素某一位置为1
FD_SET(listenfd, &allset);
for ( ; ; ) {
rset = allset;
//select 返回有多少个读就绪事件
// rset是传入传出参数,传入的时候 告诉select我需要监听哪些事件,传出的时候rset表示事件就绪集
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
printf("nready:%d\n",nready);
if (nready < 0){
fputs("select error\n",stderr);
exit(1);
}
//看看位图里有没有连接事件
if (FD_ISSET(listenfd, &rset)) {
//客户端地址长度
cliaddr_len = sizeof(cliaddr);
//三次握手接收连接 返回connfd ,accept不阻塞
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("connfd:%d\n",connfd);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip, sizeof(cli_ip)),
ntohs(cliaddr.sin_port));
//client数组记录下connfd
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd;
break;
}
//打印client数组 有效数据
int j;
for (j = 0; j < FD_SETSIZE; j++){
if( client[j] != -1){
printf("client index=%d value=%d \n",j,client[j]);
}
}
if (i == FD_SETSIZE) {
fputs("too many clients\n", stderr);
exit(1);
}
//将位图 connfd 通过位图算法计算出来的所在位设为1
FD_SET(connfd, &allset);
//更新 maxfd
if (connfd > maxfd)
maxfd = connfd;
//更新 maxi
if (i > maxi)
maxi = i;
//如果 nready==0,说明没有事件了 ,那么没有必要再去读取客户端发送过来的数据了
if (--nready == 0)
continue;
}
printf("maxi:%d\n",maxi);
printf("maxfd:%d\n",maxfd);
//处理客户端发送过来的数据 ,遍历多路io,这在以前bio是不可能的,bio只会在read死等数据
for (i = 0; i <= maxi; i++) {
// 这个if一般不会进去,因为 0~maxi的数据都是有效的
if ( (sockfd = client[i]) < 0)
continue;
printf("sockfd:%d\n",sockfd);
//看看位图里有没有客户端发送数据事件
if (FD_ISSET(sockfd, &rset)) {
//客户端断开连接 ,这个read不阻塞
if ( (n = read(sockfd, buf, MAXLINE)) == 0) {
//关闭 sockfd
close(sockfd);
//把 allset里的 sockfd所在位 置为0
FD_CLR(sockfd, &allset);
//client数组清为-1
client[i] = -1;
} else {
//有数据发送
printf("%s\n",buf);
int j;
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);//转大写
write(sockfd, buf, n);//回写过去
}
if (--nready == 0)//如果 nready==0 ,没必要再去读数据了
break;
}
}
}
close(listenfd);
return 0;
}
客户端
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#define DEST_PORT 9999//目标地址端口号
#define DEST_IP "127.32.255.2"/*目标地址IP,这里设为本机,不一定非得是127.0.0.1,只要127开头并且不是127.0.0.0和127.255.255.255即可*/
#define MAX_DATA 100//接收到的数据最大程度
int main(){
int sockfd;
struct sockaddr_in dest_addr;
char buf[MAX_DATA];
sockfd=socket(AF_INET,SOCK_STREAM,0);
dest_addr.sin_family=AF_INET;
dest_addr.sin_port=htons(DEST_PORT);
dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero),8);
connect(sockfd,(struct sockaddr*)&dest_addr,sizeof(struct sockaddr));
printf("connect success");
while(1){
char send_buf[512] = "";
scanf("%s",&send_buf);
write(sockfd,send_buf,sizeof(send_buf));
read(sockfd,send_buf,sizeof(send_buf));
printf("client receive:%s\n",send_buf);
}
return 0;
}
centos执行
gcc -o server.out server.c
gcc -o client.out client.c
得到执行文件server.out,client.out
用xshell 对一个虚拟机开两个item窗口,一个执行./server.out ,另一个执行./client.out,可以开多个客户端通信
select深度剖析
select函数
主要研究select和fd_set,关于socket相关api较简单,这里就不涉及了
select(int nfds,fd_set* rset,fd_set* wset,fd_set* eset,const struct timeval* time);
nfds = 最大文件描述符+1
rset = 读事件集合/位图
wset = 写事件集合/位图
eset = 异常事件集合/位图
time 等待I/o的最长时间,NULL表示一直等待
timeval结构体
struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
}
fd_set在下面说,nfds较难理解
0 1 2这三个数字linux内核自己要用,所以我们执行socket方法是返回的listenfd=3,此时若执行select则nfds =3+1=4
若此时客户端1来连接并且成功三次握手,再次调用select时 nfds 应=4+1=5,故我们的程序应该设置maxfd来保存这个最大的文件描述符,之所以select需要这个参数,是因为select轮询的时候需要上限。
fd_set位图
这个位图很难理解,我刚开始一直卡在这里,因为网上大多数人根本不懂位图,只会说一些概念
大多数博客都是像下面这样一笔带过
FD_CLR( s, *set) //把位图set s位清零;
FD_ISSET( s, *set) //检查文件描述符s是否存在与set中;
FD_SET( s, *set ) //把文件描述符s添加到set中;
FD_ZERO( *set ) //把set位图初始为空.
可是看完之后一点帮助都没有,为什么这么说,因为你完全不知道位图底层怎么实现的,导致服务器的代码你怎么也看不懂。
如何理解位图呢?
可以把位图理解为一张32*32的表格,表格里的数字不是0就是1,0~1023的数都可以扔到这张图里面,并且如果你把0到1023的数全都扔到这张图里面,你会发现这张表格全部变成1了。
那么如何将一个1024以内的数定位到这张表格的某个格子就显得尤为重要了?
如上图所示:有32列,8行,还有24行省略。
假若文件描述符是3,该把位图哪个格子置为1呢,行号=3/32=0,列号=3%32=3,所以就是第一行上色的那个格子
又假若文件描述符是34,行号=34/32=1,列号=34%32=2,第二行上色的即是
在此特别感谢scope
他的文章对我理解位图帮助很大,以下是他的一些见解
#define __NFDBITS (8 * sizeof(unsigned long)) //每个ulong型可以表示多少个bit,c的ulong四个字节, 故__NFDBITS = 8*32
#define __FD_SETSIZE 1024 //socket最大取值为1024
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)//bitmap一共有1024个bit,共需要多少个ulong ,__FDSET_LONGS =1024/32=32
typedef struct {
unsigned long fds_bits [__FDSET_LONGS]; //用ulong数组来表示bitmap,数组大小为32,即位图的32行,每一个数组元素32位,即位图的列
} __kernel_fd_set;
typedef __kernel_fd_set fd_set;
//每个ulong为32位,可以表示32个bit。
//fd >> 5 即 fd / 32,找到对应的ulong下标i;fd & 31 即fd % 32,找到在ulong[i]内部的位置
#define __FD_SET(fd, fdsetp) (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] |= (1<<((fd) & 31))) //设置对应的bit
#define __FD_CLR(fd, fdsetp) (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] &= ~(1<<((fd) & 31))) //清除对应的bit
#define __FD_ISSET(fd, fdsetp) ((((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] & (1<<((fd) & 31))) != 0) //判断对应的bit是否为1
#define __FD_ZERO(fdsetp) (memset (fdsetp, 0, sizeof (*(fd_set *)(fdsetp)))) //memset bitmap
由此我又想起我以前实现过一个o(n)时间复杂度java数组判断重复的算法,就用到了位图的概念,只是自己一下没有联想起来
下面是java版位图去重
粒度大的位图
这种粒度太大了,且需要判断的数最大是多少,数组就得定义多大,核心思想是将arr数组元素值当作tmp的下标使用
例如arr={1,1,2}第二次循环就返回false回去了,因为下标1已经有人占掉了
public final static int MAX = 1024;
public static boolean isDuplicate(int[] arr){
int[] tmp = new int[MAX];
for(int i=0;i<arr.length;i++){
if(tmp[arr[i]]==0){
tmp[arr[i]] = 1;
}else{
return true;
}
}
return false;
}
粒度小的位图
例如 arr={3,3}
tmp = int[32]
第一次循环:
index = 3>>5 = 0,说明3应该放在第0行
pos = 3%32 = 3 ,说明3应该放在tmp[0]的第3位(从0开始算)
if不进
tmp[0]|=(1<<3)即tmp[0] = 8
第二次循环
index = 0
pos = 3
if(8&8!=0){
return true
}
public static boolean isDuplicateByBit(int []arr){
int[] tmp = new int[MAX/32];
for(int i=0;i<arr.length;i++){
int index = arr[i]>>5 ;//数组下标
int pos = arr[i]%32;//数组下标pos对应的数的第多少位
if((tmp[index]&((1<<pos)))!=0){
return true;
}
tmp[index] |= (1<<pos);//将数组下标index对应的数的第pos位 置为1
}
return false;
}
在看一下java版的FD_SET,FD_ISSET,FD_CLR
//探测num第pos位是不是1(pos从0开始算)
public static boolean FD_ISSET(int pos,int num){
return (num&((1<<pos)))!=0;
}
public static int FD_SET(int pos,int num){
return num |= (1<<pos);
}
public static int FD_CLR(int pos,int num){
return num &= ~(1<<pos);
}
public static void main(String[] args) {
System.out.println(FD_ISSET(0,3));
System.out.println(FD_SET(2,3));
System.out.println(FD_CLR(1,30));
}
上面对于pos这个数字只能是0~31,因为num只有32位
所以select 的unsigned long fds_bits []要想支持1024个文件描述符,这个数组大小就必须是32