• 网络编程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 的阻塞模式和非阻塞模式

  • 相关阅读:
    回调函数
    箭头函数和普通函数的区别?
    实现 (5).add(3).minus(2),使其输出结果为:6
    call 和 apply 的区别?哪个性能更好?
    用 JS 的 RegExp 进行正则判断,字符串内是否有特殊符号
    如何理解和描述“原型和原型链”
    JS 的防抖和节流
    Linux 服务器 关闭防火墙命令
    前端在Linux服务器搭建安装Nginx的步骤记录
    记录一些CSS的实用技巧
  • 原文地址:https://www.cnblogs.com/zhangkele/p/10630080.html
Copyright © 2020-2023  润新知