Linux下C++进行socket连接,多进程+阻塞IO、select进行IO多路复用

服务器创建步骤

在B站上看见此视频记录一下,之前并未将socket弄明白,观看之后略懂一二;
【IO多路复用是什么?如何设计一个高性能服务器?】 https://www.bilibili.com/video/BV1WF411R7aK/?share_source=copy_web&vd_source=be53caa3c278a0606cdc59931333e597
【腾讯面试:同步阻塞(BIO)、同步非阻塞(NIO)和Select IO多路复用的原理和不同点】 https://www.bilibili.com/video/BV15X4y1Y7T9/?share_source=copy_web&vd_source=be53caa3c278a0606cdc59931333e597
socket编程的服务器端一般步骤:
在服务器中accept()等待客户端连接为阻塞类型,read()也为阻塞接收;

socket()=>bind()=>listen()=>accept()=>read()=>close(accept)=>close(listen)
1、创建一个socket,用函数socket();   
可选 2、设置socket属性,用函数setsockopt(); *
3、绑定IP地址、端口等信息到socket上,用函数bind();  
4、开启监听连接,用函数listen();   
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();   
7、关闭客户端连接;   
8、关闭监听;

socket编程的客户端一般步骤是:

socket()=>connect()=>write()=>close()
1、创建一个socket,用函数socket();   
2、设置socket属性,用函数setsockopt();*  
3、绑定IP地址、端口等信息到socket上,用函数bind();*
4、设置要连接的对方的IP地址和端口等属性;   
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();   
7、关闭网络连接;

Socket建议接收发送搭配

接受:
	nbytes = recv(sockfd, buff, buff_size,MSG_WAITALL);
发送:
	nbytes = send(scokfd, buff, buff_size,MSG_WAITALL);

read 原则:数据在不超过指定的长度的时候有多少读多少,没有数据则会一直等待。所以读取数据都需要采用循环读的方式读取数据,因为一次read 完毕不能保证读到我们需要长度的数据,read 完一次需要判断读到的数据长度再决定是否还需要再次读取。

recv 原则:正常情况下recv 等待直到读取到buff_size 长度的数据,但是recv中有个WAITALL ,表示尽量读全,在有中断的情况下recv 还是可能会被打断,造成没有读完指定的buff_size的长度。所以即使是采用recv + WAITALL 参数还是要考虑是否需要循环读取的问题,在实验中对于多数情况下recv (使用了MSG_WAITALL)还是可以读完buff_size。

Read和Recv区别

  1. 接口不同
    • read() 是标准IO函数,来自unistd.h,可以用于文件和socket。
    • recv() 是socket特有的系统调用,来自sys/socket.h
  2. 读取字节数
    • read() 每次都会尝试读取请求的全部字节数。(超出(240*384)时数据读不完
    • recv() 可能只读取部分数据,需要检查返回值。
  3. 作用对象
    • read() 用于所有文件描述符,包括socket。
    • recv() 只能用于socket描述符。
  4. 标志差异
    • recv() 支持MSG_PEEK等额外标志。
  5. 总结
    • 用于socket时,recv()更为合适,可以处理消息边界,支持标志。
    • 但read()更通用,可以用于文件和socket。

Recv函数

recv先检查套接字的接收缓冲区,如果该接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。可以设置为非阻塞;

  • 功能:用于从文件描述符对应的文件读取数据

  • 函数体:**int** **recv**(**int** sockfd, **char** ***** buf, **int** len, **int** flags);

    1. sockfd 是文件描述符。
    2. buf 是接收数据的缓冲区地址。
    3. len 表示期望读取的字节数。
    4. flags 指定调用方式,通常为0,或MSG_WAITALL
    flags说明
    MSG_DONTROUTE绕过路由表查找
    MSG_DONTWAIT仅本操作非阻塞
    MSG_OOB发送或接收带外数据
    MSG_PEEK窥看外来消息
    MSG_WAITALL等待所有数据
  • 返回值

    1. <0 错误
    2. =0 连接关闭
    3. >0 接收到的数据长度大小

阻塞IO 服务器

一个简单的 server端,仅能保持单个客户端连接并信息打印;

服务器代码server.cpp

#include<iostream>
#include<sys/socket.h>
#include<netinet/in.h>
//#include<arpa/inet.h>
//#include<unistd.h>

using namespace std;

int main()
{
	 // 创建socket
    int isockt_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(isockt_fd < 0)
    {
        cout<< "socket create err ...";
	return 0;
    }
	// 设置端口等信息
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);	// 端口号
    addr.sin_addr.s_addr = htons(INADDR_ANY);
	// 绑定ip地址
    int res = bind(isockt_fd, (struct sockaddr*)& addr, sizeof(addr));
    if(res < 0)
    {
	cout<< "bind err res = "<< res <<endl;
	return 0;
    }
	cout<< "创建成功"<<endl;
	// 监听客户端连接
    listen(isockt_fd, 30);
    
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    // 阻塞等待客户端连接
    int fd = accept(isockt_fd, (struct sockaddr*)& client, &len);
    if(fd < 0)
    {
    	cout<< "accept err res = " << fd <<endl;
	return 0;
    }
cout<<"链接成功"<<endl;
    char buffer[1024];
	// 阻塞方式接收
    read(fd, buffer, sizeof(buffer));

    cout<<buffer<<endl;
	close(fd);
    close(isockt_fd);
}

客户端代码client.cpp

#include<iostream>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string>

using namespace std;

int main()
{

    int isock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(isock_fd < 0)
    {
      	cout<< "sockt err ..."<<endl;
	return 0;
    }
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = inet_addr("10.10.77.47");
    
    int res = connect(isock_fd, (struct sockaddr*)& addr, sizeof(addr));
    if(res < 0)
    {
	cout<< "connect err ..."<<endl;
	return 0;
    }
    cout<< "链接成功"<<endl;
    string data;
    cout<< "in:";
    cin >> data;
    write(isock_fd, data.c_str(), data.size());
    close(isock_fd);
   	return 0;
}
server编译:g++ server.cpp -o server
client编译:g++ client.cpp -o client
上述方式由于accept()和read()阻塞只能进行单个连接接收,

多进程方式+阻塞IO 服务器

那如何使多个客户端对服务器进行连接呢,那我们就需要使用线程对accept()函数进行多次调用链接,同时将read()也进行循环调用,循环接收进行业务处理

服务器代码server.cpp

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <thread>

using namespace std;

int main()
{
    int isockt_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (isockt_fd < 0)
    {
        cout << "socket create err ...";
        return 0;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);    // 端口号
    addr.sin_addr.s_addr = htons(INADDR_ANY);

    int res = bind(isockt_fd, (struct sockaddr *)&addr, sizeof(addr));
    if (res < 0)
    {
        cout << "bind err res = " << res << endl;
        return 0;
    }
    cout << "创建成功" << endl;

    // 监听连接请求
    listen(isockt_fd, 30);
    cout << "监听连接请求" << endl;

    // 循环等待新连接accept
    while (1)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // accept 阻塞等待客户端 连接
        int fd = accept(isockt_fd, (struct sockaddr *)&client, &len);
        // 使用detach()方式将线程立即执行
        std::thread([&]()
                    {
            // 需要获取fd的值,不能直接使用fd,(由于Lambda表达式使用&接收后线程都会使用相同的fd描述符)
            int fd_thread = fd; 
            if (fd_thread < 0)
            {
                cout << "accept err res = " << fd_thread << endl;
                return 0;
            }
            cout << "链接成功" << endl;
            while(1)
            {
                char buffer[1024];
                // read阻塞方式接收
                int read_len = read(fd_thread, buffer, 1024);
                if (read_len == 0) // 断开连接
                {
                    cout<<"断开连接"<<endl;
                    close(fd_thread);
                }
                else // 返回字节数 -- 业务逻辑处理
                {
                    cout << buffer << endl;
                    memset(buffer, '\0', sizeof(buffer)); // 清空数组
                }
            }
            
             }).detach();
    }

    close(isockt_fd);
}

编译:g++ server.cpp -o server -lpthread

在这里插入图片描述

IO多路复用select 服务器

select函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你,系统提供select函数来实现多路复用输入/输出模型
#include <sys/time.h> 
#include <unistd.h>   

int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);

1、maxfd:是需要监视的最大的文件描述符值+1

2、rdset:读文件描述符监听集合,传入、传出参数

3、wrset:写文件描述符监听集合,传入、传出参数

4、exset:异常文件描述符监听集合,传入、传出参数

5、timeout:指向timeval结构体的指针,通过传入的这个timeout参数来决定select()函数的三种执行方式

  1.传入的timeout为NULL,则表示将select()函数置为阻塞状态,直到我们所监视的文件描述符集合中某个文件描述符发生变化是,才会返回结果。

  2.传入的timeout为0秒0毫秒,则表示将select()函数置为非阻塞状态,不管文件描述符是否发生变化均立刻返回继续执行。

  3.传入的timeout为一个大于0的值,则表示这个值为select()函数的超时时间,在timeout时间内一直阻塞,超过时间即返回结果 。

fd_set类型四个宏操作:(fd为文件描述符)

1、void FD_ZERO(fd_set *fdset);将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。

2、void FD_SET(int fd, fd_set *fdset); 用于在文件描述符集合中增加一个新的文件描述符。

3、void FD_CLR(int fd, fd_set *fdset);用于在文件描述符集合中删除一个文件描述符。

4、int  FD_ISSET(int fd, fd_set *fdset); 用于测试指定的文件描述符是否在该集合中

可以按照下面的思路来理解这些宏:a[maxfd-1], …, a[1], a[0]

FD_ZERO 用来将这个向量的所有元素都设置成 0;
FD_SET 用来把对应套接字 fd 的元素,a[fd]设置成 1;
FD_CLR 用来把对应套接字 fd 的元素,a[fd]设置成 0;
FD_ISSET 对这个向量进行检测,判断出对应套接字的元素 a[fd]是 0 还是 1。

其中 0 代表不需要处理,1 代表需要处理。

其中还需要使用ioctl()
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
 ioctl 是用来设置硬件控制寄存器,或者读取硬件状态寄存器的数值之类的。
 ioctl(fd, FIONREAD, &b);
 得到缓冲区里有多少字节要被读取,然后将字节数放入b里面。
 FIONREAD用于判断接收缓存中是否存在可读数据,用于accept的socket处于LISTEN状态
 而read,write 是把数据丢入缓冲区,硬件的驱动从缓冲区读取数据一个个发送或者把接收的数据送入缓冲区。

IO多路复用select

server.cpp

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <thread>
#include <sys/select.h>
#include <sys/ioctl.h>

using namespace std;

int main()
{
    int isocket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (isocket_fd < 0)
    {
        cout << "socket create err ...";
        return 0;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888); // 端口号
    addr.sin_addr.s_addr = htons(INADDR_ANY);

    int res = bind(isocket_fd, (struct sockaddr *)&addr, sizeof(addr));
    if (res < 0)
    {
        cout << "bind err res = " << res << endl;
        return 0;
    }
    cout << "创建成功" << endl;

    // 监听连接请求
    listen(isocket_fd, 30);
    cout << "监听连接请求" << endl;

    fd_set read_fds, test_fds;

    FD_ZERO(&read_fds);            // 清空数组
    FD_SET(isocket_fd, &read_fds); // 将socket添加到集合中

    // 循环等待新连接accept
    while (1)
    {
        test_fds = read_fds; // 将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
        // FD_SETSIZE:系统默认的最大文件描述符,进行阻塞当accept()有连接时,进行向下触发
        select(FD_SETSIZE, &test_fds, NULL, NULL, NULL);

        for (int fd = 0; fd < FD_SETSIZE; fd++)
        { // i表示文件描述符
            if (FD_ISSET(fd, &test_fds))
            {
                // 判断是否为socket监听触发
                if (fd == isocket_fd)
                {
                    // 触发accept客户端连接
                    struct sockaddr_in client;
                    socklen_t len = sizeof(client);
                    // accept 阻塞等待客户端 连接
                    int client_fd = accept(isocket_fd, (struct sockaddr *)&client, &len);
                    if (client_fd < 0)
                    {
                        cout << "accept err res = " << client_fd << endl;
                        return 0;
                    }
                    FD_SET(client_fd, &read_fds); // 将客户端fd描述符加入到read_fds集合中
                    cout << "链接成功" << endl;
                }
                else // 客户端有数据请求触发
                {
                    /*
                    得到缓冲区里有多少字节要被读取,然后将字节数放入b里面。
                    ioctl(fd, FIONREAD, &b);
                    */
                    int read_len;
                    ioctl(fd, FIONREAD, &read_len); // fd为select中客户端fd描述符,

                    if (read_len == 0) // 断开连接
                    {
                        cout << "断开连接" << endl;
                        close(fd);
                        FD_CLR(fd, &read_fds);
                    }
                    else if (read_len == -1) // 非阻塞,无数据可读
                    {
                        continue;
                    }
                    else // 返回字节数 -- 业务逻辑处理
                    {
                        char buffer[1024];
                        int read_len = read(fd, buffer, 1024);
                        cout << buffer << endl;
                        memset(buffer, '\0', sizeof(buffer)); // 清空数组
                    }
                }
            }
        }

    }

    close(isocket_fd);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值