select 函数重难点解析

select 函数重难点解析 select 函数是网络通信编程中非常常用的一个函数,因此应该熟练掌握它。虽然它是 BSD 标准之一的 Socket 函数之一,但在 Linux 和 Windows 平台,其行为表现还是有点区别的。我们先来看一下 Linux 平台上的 select 函数。 Linux 平台下的 select 函数 select 函数的作用是检测一组 socket 中某个或某几个是否有“事件”,这里的“**事件”**一般分为如下三类: 可读事件,一般意味着可以调用 recv 或 read 函数从该 socket 上读取数据;如果该 socket 是侦听 socket(即调用了 bind 函数绑定过 ip 地址和端口号,并调用了 listen 启动侦听的 socket),可读意味着此时可以有新的客户端连接到来,此时可调用 accept 函数接受新连接。 可写事件,一般意味着此时调用 send 或 write 函数可以将数据“发出去”。 异常事件,某个 socket 出现异常。 函数签名如下: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 参数说明: 参数 nfds, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数监听的 fd 中最大 fd 值加 1。 参数 readfds,需要监听可读事件的 fd 集合。 参数 writefds,需要监听可写事件的 fd 集合。 参数 exceptfds,需要监听异常事件 fd 集合。 readfds、writefds 和 exceptfds 类型都是 fd_set,这是一个结构体信息,其定义位于 /usr/include/sys/select.h 中: /* The fd_set member is required to be an array of longs. */ typedef long int __fd_mask; /* Some versions of <linux/posix_types.h> define this macros. */ #undef __NFDBITS /* It's easier to assume 8-bit bytes than to get CHAR_BIT. */ #define __NFDBITS (8 * (int) sizeof (__fd_mask)) #define __FD_ELT(d) ((d) / __NFDBITS) #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) /* fd_set for select and pselect. */ typedef struct { /* XPG4.2 requires this member name. Otherwise avoid the name from the global namespace. */ #ifdef __USE_XOPEN __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->fds_bits) #else // 在我的centOS 7.0 系统中的值: // __FD_SETSIZE = 1024 //__NFDBITS = 64 __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->__fds_bits) #endif } fd_set; /* Maximum number of file descriptors in 'fd_set'. */ #define FD_SETSIZE __FD_SETSIZE 我们假设未定义宏 __USE_XOPEN,将上面的代码整理一下: typedef struct { long int __fds_bits[16]; } fd_set; 将一个 fd 添加到 fd_set 这个集合中需要使用 FD_SET 宏,其定义如下: void FD_SET(int fd, fd_set *set); 其实现如下: #define FD_SET(fd,fdsetp) __FD_SET(fd,fdsetp) FD_SET 在内部又是通过宏 __FD_SET 来实现的,__FD_SET 的定义如下(位于 /usr/include/bits/select.h 中): #if defined __GNUC__ && __GNUC__ >= 2 # if __WORDSIZE == 64 # define __FD_ZERO_STOS "stosq" # else # define __FD_ZERO_STOS "stosl" # endif # define __FD_ZERO(fdsp) \ do { \ int __d0, __d1; \ __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ : "=c" (__d0), "=D" (__d1) \ : "a" (0), "0" (sizeof (fd_set) \ / sizeof (__fd_mask)), \ "1" (&__FDS_BITS (fdsp)[0]) \ : "memory"); \ } while (0) #else /* ! GNU CC */ /* We don't use `memset' because this would require a prototype and the array isn't too big. */ # define __FD_ZERO(set) \ do { \ unsigned int __i; \ fd_set *__arr = (set); \ for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \ __FDS_BITS (__arr)[__i] = 0; \ } while (0) #endif /* GNU CC */ #define __FD_SET(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) #define __FD_CLR(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d))) #define __FD_ISSET(d, set) \ ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0) 重点看这一行: ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) __FD_MASK 和 __FD_ELT 宏在上面的代码中已经给出定义: #define __FD_ELT(d) ((d) / __NFDBITS) #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) __NFDBITS 的值是 64 (8 * 8),也就是说 __FD_MASK (d) 先计算 fd 与 64 的余数 n,然后执行 1 « n,这一操作实际上是将 fd 的值放在 0~63 这 64 的位置上去,这个位置索引就是 fd 与 64 取模的结果;同理 __FD_ELT(d) 就是计算位置索引值了。举个例子,假设现在 fd 的 值是 57,那么在这 64 个位置的 57 位,其值在 64 个长度的二进制中置位是: 0000 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 这个值就是 1 « (57 % 64) 得到的数字。 但是前面 fd 数组的定义是: typedef struct { long int __fds_bits[16]; //可以看成是128 bit的数组 } fd_set; long int 占 8 个字节,一个 16 个 long int,如果换成二进制的位(bit)就是 8 * 16 = 128, 也就是这个数组只用了低 64 位, 高 64 位并没有使用。这说明在我的机器上,select 函数支持操作的最大 fd 数量是 64。 同理,如果我们需要从 fd_set 上删除一个 fd,我们可以调用 FD_CLR,其定义如下: void FD_CLR(int fd, fd_set *set); 原理和 FD_SET 相同,即将对应的标志位由1变0即可。 如果,我们需要将 fd_set 中所有的 fd 都清掉,则使用宏 FD_ZERO: void FD_ZERO(fd_set *set); 当 select 函数返回时, 我们使用 FD_ISSET 宏来判断某个 fd 是否有我们关心的事件,FD_ISSET 宏的定义如下: ...

2021-01-11

socket 的阻塞模式和非阻塞模式

socket 的阻塞模式和非阻塞模式 对 socket 在阻塞和非阻塞模式下的各个函数的行为差别深入的理解是掌握网络编程的基本要求之一,是重点也是难点。 阻塞和非阻塞模式下,我们常讨论的具有不同行为表现的 socket 函数一般有如下几个,见下表: connect accept send (Linux 平台上对 socket 进行操作时也包括 write 函数,下文中对 send 函数的讨论也适用于 write 函数) recv (Linux 平台上对 socket 进行操作时也包括 read 函数,下文中对 recv 函数的讨论也适用于 read 函数) 限于文章篇幅,本文只讨论 send 和recv函数,connect 和 accept 函数我们将在该系列的后面文章中讨论。在正式讨论之前,我们先解释一下阻塞模式和非阻塞模式的概念。所谓阻塞模式,就当某个函数“执行成功的条件”当前不能满足时,该函数会阻塞当前执行线程,程序执行流在超时时间到达或“执行成功的条件”满足后恢复继续执行。而非阻塞模式恰恰相反,即使某个函数的“执行成功的条件”不当前不能满足,该函数也不会阻塞当前执行线程,而是立即返回,继续运行执行程序流。如果读者不太明白这两个定义也没关系,后面我们会以具体的示例来讲解这两种模式的区别。 如何将 socket 设置成非阻塞模式 无论是 Windows 还是 Linux 平台,默认创建的 socket 都是阻塞模式的。 在 Linux 平台上,我们可以使用 fcntl() 函数或 ioctl() 函数给创建的 socket 增加 O_NONBLOCK 标志来将 socket 设置成非阻塞模式。示例代码如下: int oldSocketFlag = fcntl(sockfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; fcntl(sockfd, F_SETFL, newSocketFlag); ioctl() 函数 与 fcntl() 函数使用方式基本一致,这里就不再给出示例代码了。 当然,Linux 下的 socket() 创建函数也可以直接在创建时将 socket 设置为非阻塞模式,socket() 函数的签名如下: int socket(int domain, int type, int protocol); 给 type 参数增加一个 SOCK_NONBLOCK 标志即可,例如: int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP); 不仅如此,Linux 系统下利用 accept() 函数返回的代表与客户端通信的 socket 也提供了一个扩展函数 accept4(),直接将 accept 函数返回的 socket 设置成非阻塞的。 ...

2021-01-11

TCP 协议如何解决粘包、半包问题

TCP 协议如何解决粘包、半包问题 一 TCP 协议是流式协议 很多读者从接触网络知识以来,应该听说过这句话:TCP 协议是流式协议。那么这句话到底是什么意思呢?所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要我们人为地去给这些协议划分边界。 举个例子,A 与 B 进行 TCP 通信,A 先后给 B 发送了一个 100 字节和 200 字节的数据包,那么 B 是如何收到呢?B 可能先收到 100 字节,再收到 200 字节;也可能先收到 50 字节,再收到 250 字节;或者先收到 100 字节,再收到 100 字节,再收到 200 字节;或者先收到 20 字节,再收到 20 字节,再收到 60 字节,再收到 100 字节,再收到 50 字节,再收到 50 字节…… 不知道读者看出规律没有?规律就是 A 一共给 B 发送了 300 字节,B 可能以一次或者多次任意形式的总数为 300 字节收到。假设 A 给 B 发送的 100 字节和 200 字节分别都是一个数据包,对于发送端 A 来说,这个是可以区分的,但是对于 B 来说,如果不人为规定多长为一个数据包,B 每次是不知道应该把收到的数据中多少字节作为一个有效的数据包的。而规定每次把多少数据当成一个包就是协议格式规范的内容之一。 经常会有新手写出类似下面这样的代码: 发送端: //...省略创建socket,建立连接等部分不相关的逻辑... char buf[] = "the quick brown fox jumps over a lazy dog."; int n = send(socket, buf, strlen(buf), 0); //...省略出错处理逻辑... 接收端: //省略创建socket,建立连接等部分不相关的逻辑... char recvBuf[50] = { 0 }; int n = recv(socket, recvBuf, 50, 0); //省略出错处理逻辑... printf("recvBuf: %s", recvBuf); 为了专注问题本身的讨论,我这里省略掉了建立连接和部分错误处理的逻辑。上述代码中发送端给接收端发送了一串字符”the quick brown fox jumps over a lazy dog.“,接收端收到后将其打印出来。 ...

2021-01-11

TeamTalk源码解析

TeamTalk源码解析 01 TeamTalk介绍 02 服务器端的程序的编译与部署 03 服务器端的程序架构介绍 04 服务器端db_proxy_server源码分析 05 服务器端msg_server源码分析 06 服务器端login_server源码分析 07 服务器端msfs源码分析 08 服务器端file_server源码分析 09 服务器端route_server源码分析 10 开放一个TeamTalk测试服务器地址和几个测试账号 11 pc客户端源码分析

2021-01-11

不定参数函数实现var_arg系列的宏

不定参数函数实现var_arg系列的宏 电驴的源码日志模块有一个叫 DebugLogError 函数,其签名如下: //代码位于easyMule-master/src/WorkLayer/Log.h 55行 void DebugLogError(LPCTSTR pszLine, ...); 电驴的源码可以在公众号【 高性能服务器开发 】后台回复“获取电驴源码”即可获取。 这个函数的申明在 Log.h 头文件中,是一个全局函数,其实现代码在 Log.cpp 文件中: //代码位于easyMule-master/src/WorkLayer/Log.cpp 111行 void DebugLogError(LPCTSTR pszFmt, ...) { va_list argp; va_start(argp, pszFmt); LogV(LOG_DEBUG | LOG_ERROR, pszFmt, argp); va_end(argp); } 这个函数是一个具有不定参数的函数(也就是参数个数不确定),比如调用这个函数我们可以传入一个参数,也可以传入二个或者三个参数等等: DebugLogError(L"我喜欢你!"); DebugLogError(L"我喜欢你!", L"你喜欢谁?"); DebugLogError(L"我喜欢你!", L"你喜欢谁?", L"萧雨萌!"); 与此类似, C 语言中最熟悉的函数 printf() 和 scanf() 就是能传入不定参数的函数的例子,可是你知道如何编写这样具有不定参数的函数么? 你可以通过这段代码学习到编写方法,奥秘就在DebugLogError()中使用的几个你从来没见过的宏,让我们欢迎它们: va_list va_start va_end 这几个宏是C函数库提供的,位于头文件stdarg.h中。下面我们利用这几个宏自定义一个ShowLove()函数: #include <stdio.h> #include <tchar.h> #include <stdarg.h> #include <locale.h> int ShowLove(wchar_t* szFirstSentence, ...) { //用来统计可变参数数量 int num = 0; //第一步: //申明一个va_list类型对象ap,用于对参数进行遍历 va_list ap; //第二步: //使用va_start对变量进行初始化 //这里需要注意的是: //在传统C语言中,va_start把ap中内部指针设置为传递给函数参数的【第一个实参】; //而在标准的C中,va_start接受一个额外参数,也就是最后一个【固定参数】的名称, //并把ap中的内部指针设置为传递给函数的第一个【可变参数】. //所以你在VC++ 6.0和VS2008等高版本的编译器中使用va_start需要注意区别 //这里使用标准C va_start(ap, szFirstSentence); //第三步: //使用va_arg宏返回实参列表中的下一个参数值,并把ap的内部指针推向下一个参数(如果有的话) //必须指定下一个参数的类型。 //在调用va_start之后第一次调用va_arg将返回第一个可变参数的值 wprintf(szFirstSentence); wchar_t* p = 0; while(p = va_arg(ap, wchar_t*)) { wprintf(L"%s", p); num ++; } //第四步: //待所有可变参数都读取完毕以后,调用va_end宏对ap和va_list做必要的清理工作 va_end(ap); return num; } int main(int argc, char* argv[]) { setlocale(LC_ALL, ""); int z = ShowLoveL"我喜欢你!\n"); int y = ShowLove(L"我喜欢你!", L"你喜欢谁?\n"); int l = ShowLove(L"我喜欢你!", L"你喜欢谁?", L"萧雨萌!\n"); printf("可变参数个数依次是:%d\t%d\t%d\n", z, y, l); return 0; } 上述代码的运行结果是: ...

2021-01-11

程序员必知必会的网络命令

程序员必知必会的网络命令 利用telnet命令发电子邮件 做Java或者C++开发都应该知道的lsof命令 Linux网络故障排查的瑞士军刀nc命令 Linux tcpdump使用详解 从抓包的角度分析connect函数的连接过程 服务器开发中网络数据分析与故障排查经验漫谈

2021-01-11

程序员的烦心事

程序员的烦心事 拒绝了一家公司的offer后,他们的副总和hr总监同时打电话来询问拒绝原因并极力要求加入,我该不该去? 我是一名程序员,结婚时女友要求我用两年的工资作为彩礼,我该不该答应?

2021-01-11

程序员的薪资与年终奖那些事儿

程序员的薪资与年终奖那些事儿 技术面试与HR谈薪资技巧 聊一聊程序员如何增加收入 谈一谈年终奖

2021-01-11

程序员面试题精讲

程序员面试题精讲 腾讯后台开发实习生技能要求 聊聊如何拿大厂的 offer 网络通信题目集锦 我面试后端开发经理的经历 Linux C/C++后端开发面试问哪些问题

2021-01-11

程序员如何写简历

程序员如何写简历 笔者工作多年后面试了很多公司,例如 2018 年年初横扫各大互联网公司,也作为面试官面试了很多人,看过不少的简历。现在疫情快过去了,很多小伙伴开始准备简历看新机会了,但是不少小伙伴遇到以下两种情况: 投了很多公司,邀请面试的寥寥无几; 面试的时候被面试官问的哑口无言。 造成以上原因很大一部分是因为简历的问题,本文将结合自身的面试和被面试的经历和大家聊一聊简历怎么写。我们先来分析一些简历素材。 简历一 这是一位毕业生的简历,大家看下这个简历存在什么问题? 分析: 简历中写了自己做的一个项目,项目描述中将该项目描述成 RPC、分布式网络框架,试问从项目描述来看,哪里体现出该项目使用了 RPC 框架和分布式?且不说没用到,就算用了,一般按大多数应届生的经验水平是很难在面试时经得住面试官在分布式等问题上的追问的,这非常容易给自己面试挖坑,一般校招或者对应届生的项目要求并不会太高,但是自己在简历中写上这些“分布式”、“RPC”等高大上的术语,如果实际并未掌握,只能是给自己埋雷。 另外,求职者的项目是一个网络通信库,但是通信协议不是自己的(Protobuf),网络库也是别人的(Muduo),那这个项目有自己的东西吗?一般作为面试官对应届生没有多少项目经验是可以理解的,但是如果把别人的东西拿来自己加个壳,并写在简历中,这就没多大意义了。如果该同学尝试自己设计了一种通信协议,哪怕最终实现的不是很好,面试官也可能非常喜欢,因为融入了自己的创作和思考;退一步说,用 protobuf 也是可以的,如果面试时能说得清楚 protobuf 的序列化和反序列化的原理和该库的结构,也是 OK 的。 面试结果: 该同学在面试时因这个项目被面试官死怼,铩羽而归。 简历二 分析: 这个简历我第一眼看到之后,我猜想应该很少有 HR 或者猎头联系该同学面试吧,后来和当事人确认下,果不其然。该简历的问题有以下几点: 简历中列举的技术栈非常多,如 Linux 、Shell、Python、C++、Golang、Django、Flask、Bootstrap、JQuery……面试者真的掌握了这么多技术吗?另外,求职目标写的是“后端开发”,虽然 HR 可能不知道 Bootstrap、vue 等是前端技术,但技术面试官不知道吗?你一个求职后端职位的,你写许多前端技术干嘛?体现自己全栈吗?按作者的年龄和工作经历,很多技术只是了解或者使用过,并不一定掌握,且不说面试容易被问到而答不出来,最主要的是这份简历让人一看就觉得求职者没有自己专精的领域。说白了,啥可能都知道,但啥都没掌握好。所以大多数公司看到这样一份简历直接就 pass 掉了。 求职目标写的是“后端开发”,位置不够显眼,其次求职目标后端开发一词描述太泛,这位同学本意是求职 C++ 后台开发,但是这样一写,php、Java、golang、python 等不算后端开发吗?所以建议把求职的职位稍微缩小点范围。 面试结果: 基本无面试邀请。 简历三 分析 同学醒醒,你已经毕业工作三年了,还把毕业的硕士论文贴到简历中。。。。。。问题是,你这个毕业论文中还有 “ demo” 字样,可能你的毕业论文获过奖,但是大多数 HR 和 面试官都看不懂里面的行业术语,但是一定能看懂 “demo” 这个词,demo 给大家的感觉好像高大上不起来吧。。。。。。 这位同学作为一个非科班(动物科学)转计算机行业的人,已经成功入行三年了,为啥还要把自己本科的专业放在这么明显的位置,是强调自己转行不易、很努力吗?- -! 如果你不是科班出身,或者不是名牌大学(清华、北大、复旦、武大等)毕业,尤其是毕业几年了,就不要把学历和毕业院校放在这么显眼的位置吧,可以放在“兴趣爱好”之前。 求职意向也是一样的问题; 技术专长描述的也不好,一般我们看用人单位的招聘信息,也都是先写通用技术后写专业领域的技术,所以通用技术指的是算法数据结构、操作系统原理、网络编程等等;专业的技术,指的是 C++、Java、golang、python 等语言、各种框架、开源软件等。 另外,如果长得不是特别帅的话,就不建议放自己的照片了。。。。。。 简历四 分析 这个简历看完我是真的醉了。 大哥,我知道你没有拿得出手的项目经历和技术,可是你求职的是开发岗位,你也不至于把饲养猪的经历写进简历吧,虽然有些大厂自己给员工养猪吃,但是程序开发和养猪毕竟是两码事吧。。。。。。 简历五 分析 这份简历的项目描述也得太详细了,尤其是业务部分,感觉像项目招标书或者项目售前方案。。如果你求职的是技术开发类岗位,且你求职的下家公司与你简历中的项目不是同一个类型,那就把项目业务内容写得简略点,描述项目经历时多写一些技术内容。。。。。。说实话这份简历适合去应聘项目经理,尤其是公路局的项目经理。。。。。。 简历六 分析 这是一位大哥的简历,大哥已经工作十三年了,请读者看看这个项目经历描述是否有 13 年的水平?这项目描述实在太细了,首先可能把之前公司的商业技术机密全部泄露了。。。。。其次,和上面的简历六一样,多写点技术内容少写点业务内容不行吗,简历六可以应聘项目经理,这份简历可以应聘产品经理。。。。。。需求写的太细了,你确定是要找后端开发吗。。。。。。 简历七 分析 人常说,一份文案的整洁程度可以反映一个人的细致程度。这份简历存在两个问题: 个人技能这一块分类很混乱,例如“掌握 C++ 应用,理解底层原理,部分 c11 特性”中底层原理和 C++ 应用有什么关系,完全可以分开写嘛,另外 C++11已经目前已经被广泛使用,如果你不熟悉就不要写,写熟悉部分是熟悉多少?是告诉面试官自己这方面掌握的不好吗?原本面试时面试官可能不会问,看到这个可能说不定忍不住问几个 C++11 的东西;“多线程,同步,ipc通信等”中的“同步”难道不是针对多线程讲的吗?“熟悉设计模式、策略模式、单例模式、工厂模式”中策略模式、单例模式、工厂模式难道不是设计模式的一种吗?为何和设计模式一起用顿号并列起来? 简历中标点符号一会儿中文的逗号,一会儿英文的逗号,像 C++、Linux 这样的专用名词一会儿首字母大写,一会儿小写,导致整个排版脏乱不堪。 总结 成功的方法都差不多,错误的情形千奇百怪。因文章篇幅,就不贴更多的简历了,看完上面七份简历,你是否也有类似的情形呢?下面给大家总结一下投递简历注意事项和如何写技术简历。 ...

2021-01-11