网络编程api bind函数细节 select 细节
from:
https://blog.csdn.net/analogous_love/article/category/7257412
struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) { std::cout << "bind listen socket error." << std::endl; return -1; }
上面代码的细节 INADDR_ANY 宏 就是 ‘0.0.0.0’ 假设我们在一台机器上开发一个服务器程序,使用 bind 函数时,我们有多个ip 地址可以选择。首先,这台机器对外访问的ip地址是 120.55.94.78,这台机器在当前局域网的地址是 192.168.1.104;同时这台机器有本地回环地址127.0.0.1。
如果只想让本机 访问 bind 函数中的地址 可以使用127.0.0.1 ; 若服务器只想被局域网内部的机器访问, 地址可以使用 192.168.1.104; 若希望被公网访问 那么就可以使用 0.0.0.0 or INADDR_ANY 当年部署到阿里云的时候遇到这个坑人的细节 解决了 好久 才搞定的。
上面是 bind函数在serv 端口的调用情况 问题分析
如果在客户端 调用 bind 函数 会出现 上面问题呢?
发现三个 client 进程使用的端口号仍然是系统随机分配的,也就是说绑定 0 号端口和没有绑定效果是一样的。
情形三:客户端绑定一个固定端口
我们这里使用 20000 端口,当然读者可以根据自己的喜好选择,只要保证所选择的端口号当前没有被其他程序占用即可,服务器代码保持不变,客户端绑定代码中的端口号从 0 改成 20000。
发现 client 进程确实使用 20000 号端口连接到 server 进程上去了。这个时候如果我们再开启一个 client 进程,我们猜想由于端口号 20000 已经被占用,新启动的 client 会由于调用 bind 函数出错而退出
select api使用
Linux 平台下的 select 函数
select 函数的作用是检测一组 socket 中某个或某几个是否有“事件”就绪,这里的“事件”一般分为如下三类:
读事件就绪:
1 socket 内核中,接收缓冲区中的字节数大于等于低水位标记 SO_RCVLOWAT,此时调用 recv 或 read 函数可以无阻塞的读该文件描述符, 并且返回值大于0;
2 TCP 连接的对端关闭连接,此时调用 recv 或 read 函数对该 socket 读,则返回 0;
3 侦听 socket 上有新的连接请求;
4 socket 上有未处理的错误。
写事件就绪:
1 socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于0;
3 socket 的写操作被关闭(调用了 close 或者 shutdown 函数) ( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号);
4 socket 使⽤非阻塞 connect 连接成功或失败之后;
异常事件就绪
socket 上收到带外数据。
=================================================================================
select 使用 流程
注意一些细节问题 就是
select 函数调用前后会修改 readfds、writefds 和 exceptfds 这三个集合中的内容(如果有的话),所以如果您想下次调用 select 复用这个变量,记得在下次调用前再次调用 select 前先使用 FD_ZERO 将集合清零,然后调用 FD_SET 将需要检测事件的 fd 再次添加进去。 (这里也是 select的三大缺点之一吧 相对于 epoll 模型 当然咯最主要的 不是这里 缺点 是在 while循环中的for循环检测 ,还有一个就是 内核和用户态进行 copy)
/** * select函数示例,server端, select_server.cpp */ #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> #include <sys/time.h> #include <vector> #include <errno.h> //自定义代表无效fd的值 #define INVALID_FD -1 int main(int argc, char* argv[]) { //创建一个侦听socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << "create listen socket error." << std::endl; return -1; } //初始化服务器地址 struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) { std::cout << "bind listen socket error." << std::endl; close(listenfd); return -1; } //启动侦听 if (listen(listenfd, SOMAXCONN) == -1) { std::cout << "listen error." << std::endl; close(listenfd); return -1; } //存储客户端socket的数组 std::vector<int> clientfds; int maxfd = listenfd; while (true) { fd_set readset; FD_ZERO(&readset); //将侦听socket加入到待检测的可读事件中去 FD_SET(listenfd, &readset); //将客户端fd加入到待检测的可读事件中去 int clientfdslength = clientfds.size(); for (int i = 0; i < clientfdslength; ++i) { if (clientfds[i] != INVALID_FD) { FD_SET(clientfds[i], &readset); } } timeval tm; tm.tv_sec = 1; tm.tv_usec = 0; //暂且只检测可读事件,不检测可写和异常事件 int ret = select(maxfd + 1, &readset, NULL, NULL, &tm);//细节就是 每次select调用结束后 可读 可写 异常 事件集合都需要从新设置 包括第5个超时参数 都要从新设置 if (ret == -1) { //出错,退出程序。 if (errno != EINTR) break; } else if (ret == 0) { //select 函数超时,下次继续 continue; } else { //检测到某个socket有事件 if (FD_ISSET(listenfd, &readset)) { //侦听socket的可读事件,则表明有新的连接到来 struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); //4. 接受客户端连接 int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); if (clientfd == -1) { //接受连接出错,退出程序 break; } //只接受连接,不调用recv收取任何数据 std:: cout << "accept a client connection, fd: " << clientfd << std::endl; clientfds.push_back(clientfd); //记录一下最新的最大fd值,以便作为下一轮循环中select的第一个参数 if (clientfd > maxfd) maxfd = clientfd; } else { //假设对端发来的数据长度不超过63个字符 char recvbuf[64]; int clientfdslength = clientfds.size(); for (int i = 0; i < clientfdslength; ++i)//这里也是其效率低下的原因 相对于 epoll模型来说 { if (clientfds[i] != -1 && FD_ISSET(clientfds[i], &readset)) { memset(recvbuf, 0, sizeof(recvbuf)); //非侦听socket,则接收数据 int length = recv(clientfds[i], recvbuf, 64, 0); if (length <= 0 && errno != EINTR) { //收取数据出错了 std::cout << "recv data error, clientfd: " << clientfds[i] << std::endl; close(clientfds[i]); //不直接删除该元素,将该位置的元素置位-1 clientfds[i] = INVALID_FD; continue; } std::cout << "clientfd: " << clientfds[i] << ", recv data: " << recvbuf << std::endl; } } } } } //关闭所有客户端socket int clientfdslength = clientfds.size(); for (int i = 0; i < clientfdslength; ++i) { if (clientfds[i] != INVALID_FD) { close(clientfds[i]); } } //关闭侦听socket close(listenfd); return 0; }
上面的demo 对select调用 第五个超时参数的设置需要注意一点细节就是 传入参数为 3种情况
1.非0值的 就是检测 等待到相对应的时间就返回
2.传入为NULL 参数设置为 NULL,则 select 函数会一直阻塞下去,直到我们需要的事件触发
3. 传入为0的 监测对应的检测事件 检测结束就返回 并不等待固定时间
对上面的总结 select 使用的4个注意事项
Linux select 函数的缺点也是显而易见的:
1 每次调用 select 函数,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 较多时会很大,
2 同时每次调用 select 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大;
3 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下;
4 select 函数在每次调用之前都要对传入参数进行重新设定,这样做比较麻烦而且会降低性能。
网络通信基础重难点解析 05 :socket 的阻塞模式和非阻塞模式