c++socket套接字通信

单线程非io多路复用

server.cpp

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
using namespace std;

int main()
{
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);
    addr.sin_addr.s_addr = 0;

    int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(struct sockaddr));
    if (ret == -1)
    {
        perror("bind");
        exit(0);
    }

    ret = listen(lfd, 128);
    if (ret == -1)
    {
        perror("listen");
        exit(0);
    }
    while (1)
    {
        struct sockaddr_in cliaddr;
        socklen_t clilen = sizeof(cliaddr);
        int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &clilen);
        if (cfd == -1)
        {
            perror("accept");
            exit(0);
        }

        char ip[24] = {0};
        inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip));
        int port = ntohs(cliaddr.sin_port);
        cout << ip << ":" << port << endl;

  while(1){
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        int len=read(cfd,buf,sizeof(buf));
        if(len>0){
        cout<<"client say:"<<buf<<endl;
        write(cfd,buf,len);
        }else if(len==0){
        cout<<"end connect"<<endl;
        break;
        }else{
            perror("read");
            break;
        }
    }
        close(cfd);
    }
    
    close(lfd);

    return 0;
}

client.cpp

#include <iostream>
#include <unistd.h>
#include<string.h>
#include<arpa/inet.h>
using namespace std;

int main(){
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd==-1){
perror("socket");
exit(0);
    }
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(10000);
inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr.s_addr);
int ret=connect(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret==-1){
    perror("connect");
    exit(0);
}
int number=0;
while(1){
    char buf[1024];

    sprintf(buf, "你好, 服务器...%d\n", number++);
    write(fd,buf,strlen(buf)+1);

    memset(buf,0,sizeof(buf));
    int len=read(fd,buf,sizeof(buf));
    if(len>0){
        cout<<"server say"<<buf<<endl;
    }else if(len==0){
        cout<<"connect exit"<<endl;
    }else 
    {
        perror("read");
        break;
    }
    sleep(1);
}
close(fd);


    return 0;
}

单线程一次只能接待一个client
线程在accept/read/write可以阻塞,就无法处理下一个连接。

io多路复用

select:可以处理的最大连接数1024,可以跨平台
epoll:linux,红黑树,快
poll:可处理的最大连接数无上限(视资源多寡而定),不可跨平台。

同时检测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪(可以读或者写)组赛就被解除,并基于这(一个或者多个)就绪的文件描述符进行通信。

多线程并发处理:

  • 服务器:
    主线程:监测客户端连接请求,处理accept连接,无连接则阻塞,有则唤醒建立连接
    子线程:和客户端通信
    read()/recv接收数据,无数据则阻塞(处理其他连接中有数据的),有则唤醒处理
    write()/send()给客户端发送数据,写满了则阻塞,否则等可以写了唤醒并将带发送数据写入写缓冲区中。
  • io多路复用:
    使用io多路转接函数委托内核检测服务器所有的文件描符号(通信和监听),这个监测会导致线程阻塞,如果检测到已就绪的文件描述符组赛接触,就将其传出
    监听文件描述符:和客户端建立连接
    此时调用accept()不会阻塞程序,因为文件描述符以及就绪(不用等待)
    通信的文件描述符:调用读写。

io多路复用为什么有效:在可处理时唤醒调用资源去处理,而不是空闲等待其就绪,把资源一直利用起来。在可以执行的时候唤醒资源去处理。
线程方式,用其他线程去监测是否就绪,单线程只有一个不能用它去监测是否就绪,于是把监测交给内核检测就绪的文件描述符。

优势:系统开销小,不必创建进行/线程,因此也不用维护进程线程,因此开销小。

select

特点:跨平台(Linux,Mac,Windows)

#include <sys/select.h>
struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval * timeout);

1.nfds:委托内核检测的这三个集合中最大的文件描述符+1
2.readfds:文件描述符的集合,内核之监测这个集合中文件描述符对应的读缓冲区
3.writefds:文件描述符的集合,监测其中对应的写缓冲区
4.exceptfds:监测该文件描述集合中是否有fd处于异常状态
5.timeout:超时时长,用来强制解除select函数的组赛的

NULL:函数检测不到就绪的文件描述符会一直阻塞。
等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回 0
不等待:函数不会阻塞,直接将该参数对应的结构体初始化为 0 即可。

函数返回值:0:成功
-1:调用失败
0:超时,没有检测到就绪的文件描述符

初始化fd_set

// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0        
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);
sizeof(fd_set) = 128 字节 * 8 = 1024 bit      // int [32]

在这里插入图片描述
0代表不检测该fd,1表示监测该fd的状态。

select的io多路复用实现并发流程:
1.创建监听的套接字socket,int lfd=socket();
2.lfd绑定本地ip和端口
3.listen
4.创建监听文件描述符集合fd_set,

  1. FD_zero 初始化未为0
  2. FD_Set 将坚挺的文件描述符放入集合
    5.循环调用select找就绪的文件描述符
    6.select()解除阻塞,得到内核传出的满足条件的fd集合
    通过fd_isset判断是我们关注的监听fd,则用accept与client建立连接,(此时得到通信的cfd,放入检测集合)
    如果是通信fd则通信,如果断开连接则把监听fd从检测集合删除,不断开则正常通信
    7.重复6
    在这里插入图片描述

select:

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
using namespace std;
int main()
{
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);
    addr.sin_addr.s_addr = 0;
    bind(lfd, (struct sockaddr *)&addr, sizeof(addr));

    listen(lfd, 128);

    int maxfd = lfd;
    fd_set rdset;
    fd_set rdtemp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);

    while (1)
    {
        rdtemp = rdset;
        int num = select(maxfd + 1, &rdtemp, NULL, NULL, NULL);

        if (FD_ISSET(lfd, &rdtemp))
        {
            struct sockaddr_in cliaddr;
            socklen_t len = sizeof(cliaddr);
            int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

            FD_SET(cfd, &rdset);
            maxfd = maxfd > cfd ? maxfd : cfd;
        }

        for (int i = 0; i <= maxfd; i++)
        {
            if (i != lfd && FD_ISSET(i, &rdtemp))
            {
                char buf[10] = {0};
                int len = read(i, buf, sizeof(buf));
                if (len == 0)
                {
                    cout << "client close" << endl;
                    FD_CLR(i, &rdset);
                    close(i);
                }
                else if (len > 0)
                {
                    cout << "client say:" << buf << endl;
                    write(i, buf, len);
                }
                else
                {
                    perror("read");
                }
            }
        }
    }

    return 0;
}
#include <iostream>
#include <unistd.h>
#include<string.h>
#include<arpa/inet.h>
using namespace std;

int main(){
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd==-1){
perror("socket");
exit(0);
    }
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(10000);
inet_pton(AF_INET,"127.0.0.1",&addr.sin_addr.s_addr);
int ret=connect(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret==-1){
    perror("connect");
    exit(0);
}
int number=0;
while(1){
    char buf[1024];
    sprintf(buf, "你好,服务器...%d\n", number++);
    write(fd,buf,strlen(buf)+1);

    memset(buf,0,sizeof(buf));
    int len=read(fd,buf,sizeof(buf));
    if(len>0){
        cout<<"server say"<<buf<<endl;
    }else if(len==0){
        cout<<"connect exit"<<endl;
    }else 
    {
        perror("read");
        break;
    }
    sleep(1);
}
close(fd);


    return 0;
}

epoll

linux 内核实现 IO 多路转接 / 复用(IO multiplexing)的一个实现。IO 多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。epoll 是 select 和 poll 的升级版,相较于这两个前辈,epoll 改进了工作方式,因此它更加高效。

  • select,poll对于其基于线性方式处理,epoll基于红黑树来管理待检测集合
  • select,poll每次线性扫描整个带检测集合,集合越大速度越慢,epoll使用回调机制,效率高,处理效率也不会随着检测集合的变大而下降
  • select,poll工作工程存在内核和用户空间数据的频繁拷贝问题,而epoll中内存和用户去使用共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
  • select,poll返回的集合进行判断才可以知道哪些文件描述符是就绪的epoll可以直接得到已就绪的文件描述符集合,无需再次检测。
  • epoll无最大文件描述符的限制,仅受系统中进程可以打开的最大文件数目的限制
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

select/poll低效的原因是将添加/维护待检测任务阻塞进程/线程两个步骤合二为一。每次都需要这两部,大多数场景中需要检测的socket个数相对固定,不需要每次都修改。
epoll将其分开,先用epoll_ctl()维护等待队列,再用epoll_wait()阻塞进程。

在这里插入图片描述

int epoll_create(int size);

创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。

epoll_ctl() 函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作。

// 联合体, 多个变量共用同一块内存        
typedef union epoll_data {
 	void        *ptr;
	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

struct epoll_event {
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数参数:
epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
op:这是一个枚举值,控制通过该函数执行什么操作
EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点
fd:文件描述符,即要添加 / 修改 / 删除的文件描述符
event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
events:委托 epoll 检测的事件
EPOLLIN:读事件,接收数据,检测读缓冲区,如果有数据该文件描述符就绪
EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写该文件描述符就绪
EPOLLERR:异常事件
data:用户数据变量,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
函数返回值:
失败:返回 - 1
成功:返回 0
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

监测创建的epoll实例中有无就绪的文件描述符。

函数参数:
epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
events:传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
maxevents:修饰第二个参数,结构体数组的容量(元素个数)
timeout:如果检测的 epoll 实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回
大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
-1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞
函数返回值:
成功:
等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符
大于 0:检测到的已就绪的文件描述符的总个数
失败:返回 - 1
#include <iostream>
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
using namespace std;

int main()
{
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket");
        exit(0);
    }
    struct sockaddr_in addr;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);

    // port reuse
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(0);
    }

    ret = listen(lfd, 64);
    if (ret == -1)
    {
        perror("listem error");
        exit(0);
    }

    int epfd = epoll_create(64);
    if (epfd == -1)
    {
        perror("epoll_create");
        exit(0);
    }

    // 往epoll实例中添加需要检测的节点, 现在只有监听的文件描述符
    struct epoll_event ev;
    ev.events = EPOLLIN; // 检测lfd读读缓冲区是否有数据
    ev.data.fd = lfd;
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    if (ret == -1)
    {
        perror("epoll_ctl");
        exit(0);
    }

    struct epoll_event evs[1024];
    int size = sizeof(evs) / sizeof(struct epoll_event);
    while (1)
    {
        int num = epoll_wait(epfd, evs, size, -1);
        for (int i = 0; i < num; i++)
        {
            int curfd = evs[i].data.fd;
            if (curfd == lfd)
            {
                int cfd = accept(curfd, NULL, NULL);
                ev.events = EPOLLIN;
                ev.data.fd = cfd;
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                if (ret == -1)
                {
                    perror("epoll_ctl-accept");
                    exit(0);
                }
            }
            else
            {
                char buf[1024];
                memset(buf,0,sizeof(buf));
                int len=recv(curfd,buf,sizeof(buf),0);
                if (len == 0)
                {
                    printf("客户端已经断开了连接\n");
                    // 将这个文件描述符从epoll模型中删除
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }
                else if (len > 0)
                {
                    printf("客户端say: %s\n", buf);
                    send(curfd, buf, len, 0);
                }
                else
                {
                    perror("recv");
                    exit(0);
                }
            }

        } // for
    }
    return 0;
}

当在服务器端循环调用 epoll_wait() 的时候,就会得到一个就绪列表,并通过该函数的第二个参数传出:

epoll的工作模式

水平模式:LT(level triggered)

  • 读事件:如果文件描述符对应的都缓冲区还有数据,读就会被出发
  • 因为读数据是被动的,必须通过它知道有数据到达,所以对读的检测必须的。
  • 写事件:缓冲区可写就会触发
  • 因为写时主动的,所以对写事件的检测不是必须

边沿模式:ET(edge-triggered)
高速工作模式,文件已经就绪就不会再发更多通知,减少epoll事件被重复触发的次数。
读:数据没有被全部取走且有无数据进入都不再次触发。
写:可写时只触发一次。

使用边沿模式那么需要保证内存足够大,可以一次把数据从缓冲区读出。或者把套接字设置成非组赛模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值