• c++网络编程


    c++在windows和Linux上的网络开发流程实际上都差不多,用的API也一样

    sock,bind,listen,connect,accept,aend,recv,select,gethostbyname,close

    1.TCP网络通信的流程

    服务端流程:

    (1).创建套接字

    (2).绑定IP和端口

    (3).开启监听

    (4).接受连接

    (5).基于接受而新产生的套接字来接受和发送信息

    (6).关闭套接字

    客服端

    (1).创建套接字

    (2).绑定服务器

    (3).发送信息,接受信息

    (4).close

    有个比较有趣的问题,服务器绑定的地址,如果只允许本地局域网连接,设置为127.0.0.1,如果允许外网连接,就设置为INADDR_ANY。客户端的端口和服务器的端口需要根据实际情况选择,1024以下的端口一般是预留给各种特殊程序的,所以最好不要占用。

    服务器的端口一定要选择一个确定值,因为客户端要根据具体的地址连接到服务器。

    至于客户端,它到底能不能绑定一个端口呢?

    回答是可以,但是又不可以。

    这是因为客户端也可以使用确定的端口地址来与服务器通信,如果你不去绑定,那么操作系统就随机给你分配一个。如果你绑定了,但是端口又是不合法的地址,那么操作系统又会给随机分配一个。就是这么一回事。

    一个简单的通信例子

    1. /*服务端*/
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. using namespace std;
    13. int main()
    14. {
    15. //create socket
    16. size_t socket_int = socket(AF_INET, SOCK_STREAM, 0);
    17. if(socket_int < 0)
    18. {
    19. cout << "套接字创建失败" << endl;
    20. return -1;
    21. }
    22. //服务端的地址
    23. struct sockaddr_in server_addr;
    24. memset(&server_addr, 0, sizeof(struct sockaddr_in));
    25. server_addr.sin_family = AF_INET;
    26. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    27. server_addr.sin_port = htons(3000);
    28. //绑定
    29. if(bind(socket_int, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) < 0)
    30. {
    31. cout << "bind error" << endl;
    32. return -1;
    33. }
    34. //监听
    35. if(listen(socket_int,50) < 0)
    36. {
    37. cerr << "监听失败" << endl;
    38. return -1;
    39. }
    40. while(true)
    41. {
    42. struct sockaddr_in clientaddr;
    43. socklen_t clientaddrlen = sizeof(clientaddr);
    44. //接受客户端连接
    45. int clientfd = accept(socket_int, (struct sockaddr*)&clientaddr, &clientaddrlen);
    46. if(clientfd < 0)
    47. continue;
    48. char recvBuf[255];
    49. memset(recvBuf, '\0', 255);
    50. int ret = recv(clientfd, recvBuf, 255, 0);
    51. if(ret > 0)
    52. {
    53. cout << "接收到信息:" << recvBuf << endl;
    54. ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
    55. ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
    56. }
    57. else
    58. cout << "recv error"<
    59. close(clientfd);
    60. }
    61. close(socket_int);
    62. return 0;
    63. }
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. using namespace std;
    10. int main()
    11. {
    12. //create socket
    13. size_t socket_int = socket(AF_INET, SOCK_STREAM, 0);
    14. if(socket_int < 0)
    15. {
    16. cout << "套接字创建失败" << endl;
    17. return -1;
    18. }
    19. //服务端的地址
    20. struct sockaddr_in server_addr;
    21. memset(&server_addr, 0, sizeof(struct sockaddr_in));
    22. server_addr.sin_family = AF_INET;
    23. server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    24. server_addr.sin_port = htons(3000);
    25. //绑定
    26. if(connect(socket_int, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) < 0)
    27. {
    28. cout << "bind error" << endl;
    29. return -1;
    30. }
    31. char sendMessage[] = "hello world";
    32. //发送信息
    33. int ret = send(socket_int, sendMessage, strlen(sendMessage), 0);
    34. if(ret != strlen(sendMessage))
    35. {
    36. cerr << "发送失败" << endl;
    37. return -1;
    38. }
    39. char recvBuf[255];
    40. memset(recvBuf, '\0', 255);
    41. ret = recv(socket_int, recvBuf, 255, 0);
    42. if(ret > 0)
    43. {
    44. cout << "接收到信息:" << recvBuf << endl;
    45. }
    46. else
    47. cout << "recv error"<
    48. close(socket_int);
    49. return 0;
    50. }

    2.select函数

    (1)select事件就绪

    select函数是用来检测一组socket中是否有事件就绪,事件一般分为三类

    (1).读事件

    简单来讲就是内核缓冲区中有字符写入,或者描述符读取被关闭

    (2).写事件

    内核缓冲区有可以发送的字符,或者描述符的写端关闭

    (3).异常事件

    描述符出问题了

    (2).函数描述

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

    (1).nfds:最大的事件检测值+1

    (2).readfds:需要监听的写事件的fd集合

    (3).writefds:需要监听的读事件的fd集合

    (4).exceptfds:需要监听的异常事fd集合

    timeout是一个结构体,它是一个时间设置,代表select在它规定的时间里面监听,超出这个时间就要返回。

    struct timeval{

            int tv_sec;//秒

            int tv_usec;//微秒

    };

    关于这个函数的配套函数其实都是位操作。其中的核心fd_set是一个结构体

    typedef struct{

            long int __fds_bits[16];

    }fd_set;

    long int 为8个字节,因此它可以监听1024个信息

    配套的函数都是宏定义实现的,我们先看看有哪些函数

    (1).清空函数-把全部的位清空

    void FD_ZERO(fd_set *set);

    (2).删除一个位:把对应的fd从监听中取消

    void FD_CLR(int fd, fd_set* set);

    (3).添加一个位:添加一个fd到监听的队列中

    void FD_SET(int fd, fd_set* set);

    (4).判断某个fd是否有我们关注的事件

    void FD_ISSET(int fd, fd_set* set);

    下面我们介绍一下以下用法;

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. using namespace std;
    12. int main()
    13. {
    14. //create socket
    15. size_t socket_int = socket(AF_INET, SOCK_STREAM, 0);
    16. if(socket_int < 0)
    17. {
    18. cout << "套接字创建失败" << endl;
    19. return -1;
    20. }
    21. //服务端的地址
    22. struct sockaddr_in server_addr;
    23. memset(&server_addr, 0, sizeof(struct sockaddr_in));
    24. server_addr.sin_family = AF_INET;
    25. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    26. server_addr.sin_port = htons(3000);
    27. //绑定
    28. if(bind(socket_int, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) < 0)
    29. {
    30. cout << "bind error" << endl;
    31. return -1;
    32. }
    33. //监听
    34. if(listen(socket_int,50) < 0)
    35. {
    36. cerr << "监听失败" << endl;
    37. return -1;
    38. }
    39. vector<size_t> clientfds;
    40. int maxfd;
    41. while(true){
    42. fd_set readset;
    43. FD_ZERO(&readset);
    44. //监听可读事件
    45. FD_SET(socket_int, &readset);
    46. //将客户端的事件加入到监听中
    47. maxfd = socket_int;
    48. int clientfdslength = clientfds.size();
    49. for(int i = 0; i < clientfdslength; ++i)
    50. {
    51. if(clientfds[i] != -1)
    52. {
    53. FD_SET(clientfds[i], &readset);
    54. if(maxfd < clientfds[i])
    55. maxfd = clientfds[i];
    56. }
    57. }
    58. struct timeval tm;
    59. tm.tv_sec = 1;
    60. tm.tv_usec = 0;
    61. //暂时只检测可读的事件
    62. int ret = select(maxfd+1, &readset, NULL, NULL, &tm);
    63. if(ret < 0)
    64. {
    65. cout << "select error\n";
    66. break;
    67. }
    68. else if(ret == 0)//检测超时
    69. {
    70. continue;
    71. }
    72. else
    73. {
    74. //检测事件
    75. if(FD_ISSET(socket_int,&readset))
    76. {
    77. struct sockaddr_in clientaddr;
    78. socklen_t clientlength = sizeof(struct sockaddr_in);
    79. int clientfd = accept(socket_int, (struct sockaddr*)&clientaddr, &clientlength);
    80. if(clientfd == -1)
    81. {
    82. break;
    83. }
    84. cout << "接受到连接" << endl;
    85. clientfds.push_back(clientfd);
    86. }
    87. else
    88. {
    89. char recvBuf[255] = "";
    90. size_t clientlength = clientfds.size();
    91. for(int i = 0; i < clientlength; ++i)
    92. {
    93. if(clientfds[i]!=-1 && FD_ISSET(clientfds[i], &readset))
    94. {
    95. int length = recv(clientfds[i], recvBuf, 255, 0);
    96. if(length <= 0)
    97. {
    98. cout << "数据接受失败"<
    99. close(clientfds[i]);
    100. clientfds[i] = -1;
    101. continue;
    102. }
    103. cout << "接受到的数据:" << recvBuf << endl;
    104. }
    105. }
    106. }
    107. }
    108. }
    109. int clientfdslength = clientfds.size();
    110. for(int i = 0; i < clientfdslength; ++i)
    111. {
    112. if(clientfds[i] != -1)
    113. close(clientfds[i]);
    114. }
    115. close(socket_int);
    116. return 0;
    117. }

     首先,我们创建一个套接字,然后绑定服务器的IP和端口,然后创建fd_set,把创建的套接字纳入到监听中,然后不断循环测试,如果检测到被修改的套接字,则给它建立一个连接,生成一个套接字,然后加入到监听组中,通过循环不断遍历,查找已经产生变化的套接字,然后接受他们的信息。

    需要注意的是select在调用前后可能会修改监听的套接字,需要调用FD_ZERO和FD_SET来及时设置他们。其次time的值设定问题,如果设置这个参数的事件为0,则select立刻返回,如果设置为NULL,则会阻塞。select的参数必须是最大套接字+1

    3.select的缺点

    (1)每次调用select的时候,都需要把fd集合从用户态复制到内核态,这个开销在fd较多的时候开销很大,同时每次调用select都需要在内核中遍历传递进来的所有fd,这个开销在fd较多时也很大。

    (2).能够监控的文件描述符也有限,一般是1024,也有512的,也有2048的

    (3).select调用的时候都需要对传入的参数进行重新设定,很麻烦

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

    (1).阻塞与非阻塞的区别

    当满足条件的时候,执行线程会继续进行,但是条件不满足的时候,线程是否会继续执行下去?

    如果会,则它是非阻塞。

    如果不会,则它不是阻塞。

    默认情况下,socket是阻塞的。

    (2).设置非阻塞的方法

    如果我们想要创建一个非阻塞的套接字,可以使用以下几种办法

    (1).在创建套接字的时候设置type为SOCK_NONBLOCK

    int sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

    (2).使用accept4的第四个参数为SOCK_NONBLOCK

    (3).使用万能的修改描述符函数fcntl或者ioctl函数

    (3).send和recv函数在阻塞和非阻塞的表现

    send函数是先把需要发送的字节从用户态复制到内核缓冲区,在TCP协议中,数据需要累积到一定的程度才会被发送出去,同样的recv会从内核缓冲区不断复制字节到用户区。这就要求服务方和用户方一致的表现。

    假设出现了一种情况,接收方没有读取内核缓冲区数据,那么发送方的内核缓冲区满了怎么办?

    这要分情况讨论

    (1).非阻塞模式:发送方继续调用send,然后返回一个错误记录,对于recv也一样

    (2).阻塞模式:阻塞在send处,对于recv也一样。

    这里顺便提一下三次握手和四次挥手

    (1).客户端连接服务器,发送一个seq = a

    (2).服务器接收到a, 返回一个ack=a+1, 同时发送一个seq = b

    (3).客户端接收到b,给服务器发送ack = b +1

    如果一次连接,则客服端则无法确定与服务器是否建立了连接

    如果只有两次,服务器无法确定客服端的状态

    三次就刚刚好,可以相互确认

    四次挥手还是以客服端为主动方

    (1).客户端发送一个FIN,表示不在发送数据,但是它还可以接收数据,因为你不确定服务器还发送数据不?

    (2).服务器受到到FIN,发送一个ACK,表示知道了

    (3).服务器再发送一个FIN,表示服务器也没有需要发送的数据了

    (4).客服端接收到之后,就知道自己也不会接收到服务器的数据了,于是就返回一个确认ACK,这是表示我知道你不会发送了,而我也不在接收,然后等待一会就自动关闭。

    (3)recv和send的返回值问题

    一般来讲,当返回值大于0的时候,代表成功发送的字节

    但是大于0不代表正确,这是因为有可能你想要发送(接收)20个字节,但是却只发送(接收)了10个,所有你需要重新传输。

    其次返回值为0的问题:

    返回值为0代表对方端口关闭了,或者你发送了0个字节

    最后返回值为负数:

    如果是阻塞模式,它肯定错了;但是如果是非阻塞模式,这也不代表一定错了,可能是缓冲区满了无法send或者缓冲区空了无法recv,也有可能是TCP的窗口拥塞。

    5.connect的阻塞和非阻塞模式

    在阻塞模式下,connect函数会等待到有了明确的连接结构才会返回,否则会阻塞。

    在非阻塞模式下,调用函数之后,无论成功与否,都会立刻返回,如果失败了则会返回具有明确意义的错误码,并不代表一定出错。如果我们需要在非阻塞模式下编写代码,则可以先创建阻塞模式的套接字,然后使用fcntl设置为非阻塞。如果需要再非阻塞状态下判断连接情况,就需要使用select或者poll来判断当前套接字是否可写。

    6.poll函数的用法

    poll函数也是用来检测一组描述符可读可写和异常事件的,

    (1).函数解释

    int poll(struct pollfd* fds, nfds_t nfds,int timeout);

    fds是一个结构体指针,指向数组第一位。

    nfds,类型其实是long int,代表数组长度

    timeout代表检测时间

    结构体的定义如下:

    struct pollfd{

            int fd;//事件集合

            short event;//关心的事件组合

            short revent;//检测之后得到的事件类型

    };

    event的事件类型如下

    事件事件描述是否可以作为输入是否可以作为输出
    POLLIN数据可读
    POLLOUT数据可写
    POLLRDNORM数据可读
    POLLRDBAND优先级带数据可读
    POLLPRI高级优先级数据可读
    POLLWRNORM数据可写
    POLLWRBAND优先带数据可写
    POLLRDHUPTCP连接对端关闭,或者关闭了写操作
    POLLHUP挂起
    POLLERR错误
    POLLVAL文件描述符没有打开

    poll比起select有许多优点,第一就是不用担心检测事件的数量,第二是不用重复设置监听,第三是不用买担心监听最大文件描述符最大值+1,第四是速度更快。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. int main()
    12. {
    13. //创建套接字
    14. int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    15. if(listenfd == -1)
    16. {
    17. std::cerr << "create socket error " << std::endl;
    18. return -1;
    19. }
    20. //套接字修改为非阻塞
    21. int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
    22. int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    23. if(fcntl(listenfd, F_SETFL, newSocketFlag)==-1)
    24. {
    25. close(listenfd);
    26. std::cerr<<"scoket flag change error" <
    27. }
    28. //复用IP和端口
    29. int no = 1;
    30. setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*) &no, sizeof(no));
    31. setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*) &no, sizeof(no));
    32. //设置服务器地址
    33. struct sockaddr_in server_addr;
    34. memset(&server_addr, 0, sizeof(struct sockaddr_in));
    35. server_addr.sin_family = AF_INET;
    36. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    37. server_addr.sin_port = htons(3000);
    38. //绑定
    39. if(bind(listenfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) == -1)
    40. {
    41. std::cout << "绑定失败" << std::endl;
    42. close(listenfd);
    43. return -1;
    44. }
    45. //开启监听
    46. if(listen(listenfd, 100) == -1)
    47. {
    48. close(listenfd);
    49. return -1;
    50. }
    51. //开始准备监听套接字和事件
    52. std::vector fds;
    53. pollfd listen_fd_info;
    54. listen_fd_info.fd = listenfd;//先监听服务器的套接字
    55. listen_fd_info.events = POLLIN;//监听可读事件
    56. listen_fd_info.revents = 0;
    57. fds.push_back(listen_fd_info);//放入监听数组
    58. bool exist_invalid_fd;
    59. int n;
    60. while(true)
    61. {
    62. exist_invalid_fd = false;
    63. n = poll(&fds[0], fds.size(), 1000);//检查是否有可读的套接字
    64. if(n < 0)
    65. {
    66. std::cerr << "poll is error" << std::endl;
    67. if(errno == EINTR)
    68. continue;
    69. break;
    70. }
    71. else if(n == 0) continue;
    72. //poll的缺点来了,不管有没有可读的,全都遍历一遍读取
    73. for(size_t i = 0; i < fds.size(); ++i)
    74. {
    75. //事件可读
    76. if(fds[i].events & POLLIN)
    77. {
    78. if(fds[i].fd == listenfd)//服务器套接字可读,客户端连接来了
    79. {
    80. struct sockaddr_in clientaddr;
    81. memset(&clientaddr, 0, sizeof(struct sockaddr_in));
    82. socklen_t len = sizeof(struct sockaddr_in);
    83. int clientsock = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
    84. if(clientsock != -1)
    85. {
    86. int oldSocketFlag = fcntl(clientsock, F_GETFL, 0);
    87. int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    88. if(fcntl(clientsock, F_SETFL, newSocketFlag) == -1)
    89. {
    90. close(clientsock);
    91. std::cerr<<"change client socket fail\n";
    92. }
    93. else
    94. {
    95. struct pollfd client_fd_info;
    96. client_fd_info.fd = clientsock;
    97. client_fd_info.events = POLLIN;
    98. client_fd_info.revents = 0;
    99. fds.push_back(client_fd_info);
    100. std::cout << "new client socket create successful" << std::endl;
    101. }
    102. }
    103. }
    104. else//其他事件可读
    105. {
    106. char recvBuf[100] = "";
    107. int m = recv(fds[i].fd, recvBuf, 100, 0);
    108. if(m <= 0)
    109. {
    110. if(errno != EINTR && errno != EWOULDBLOCK)
    111. {
    112. for(std::vector::iterator iter = fds.begin();iter != fds.end(); ++iter)
    113. {
    114. if(iter->fd == fds[i].fd)
    115. {
    116. std::cout << "client diconnected, clientfd:" << fds[i].fd << std::endl;
    117. close(fds[i].fd);
    118. iter->fd = -1;
    119. exist_invalid_fd = true;
    120. break;
    121. }
    122. }
    123. }
    124. }
    125. else
    126. {
    127. std::cout << "recv form client: " << recvBuf << ", clientfd: "<< fds[i].fd << std::endl;
    128. }
    129. }
    130. }
    131. else if(fds[i].revents & POLLERR)
    132. {
    133. std::cerr<< "revents is error\n";
    134. }
    135. }
    136. if(exist_invalid_fd)
    137. {
    138. for(std::vector::iterator iter = fds.begin(); iter != fds.end();)
    139. {
    140. if(iter->fd == -1)
    141. iter = fds.erase(iter);
    142. else
    143. ++iter;
    144. }
    145. }
    146. }
    147. for(std::vector::iterator iter = fds.begin(); iter != fds.end(); ++iter)
    148. close(iter->fd);
    149. }

    poll的缺点也很明显,第一,它会不管数组中的文件描述符是否发送了变化,全都从内核空间和用户空间之间整体复制;第二,poll函数返回之后,需要遍历fd集合来获取就绪的fd;第三,同时连接大量用户在某一时刻可能只有很少的就绪状态,因为随着监视的描述符数量的增长,其效率也会下降。

    7.epoll模型

    1.基本用法

    (1).创建epollfd

    int epoll_create(int size);

    size传入一个大于0的参数就行了,成功创建则返回一个非负数的epollfd;

    (2).将需要操作的fd进行操作,比如将fd绑定到epollfd上,或者解绑下来

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

    epfd:被创建出来的epollfd

    op:操作类型,比如解绑,绑定到epfd;它的取值如下:

    1).EPOLL_CTL_ADD绑定到epfd

    2).EPOLL_CTL_MOD修改fd

    3).EPOLL_CTL_DEL从epfd上解绑

    fd:被监听的描述符

    event:一个结构体地址,如果没有特殊要求可以设置为NULL

    它的结构如下:

    struct epoll_event{

            uint_32_t events;//需要检测的事件

            epoll_data_t data;//用户自定义的数据

    };

    成功调用返回0,失败返回-1

    (3).设置好fd需要检测的事件,绑定好fd之后就可以开始调用epoll_wait函数来检测事件了

    int epoll_wait(int epfd, struct epoll_event* events, int maxevent, int timeout);

    epfd:需要检测描述符集合

    events:监听的事件集合

    maxevent:最大事件数量

    timeout:监听的时间,毫秒

    函数调用成功则需要返回返回事件fd的数量,返回0代表超时,返回-1代表失败。

    2.epoll和poll的区别

    epoll_wait调用结束后,可以得到所有事件就绪的套接字,而poll在调用结束后,得到了是原本全部的套接字,需要一个一个去检查谁才是可以用的。

    3.边沿触发ET与水平触发LT

    epoll有水平触发和边缘促发,这个概念很简单,不要说不懂。

    水平触发:事件存在则不断触发

    边沿触发:事件不存在的时候,突然有了则触发,如果事件还存在则不促发;如果事件没有了,然后再次出现,则触发。简单来讲,事件状态发生了改变就触发。

    以socket读事件为例子

    水平触发条件:socket上有可读的数据

    边沿触发条件:socket上来了新的数据

    socket写事件的例子

    水平触发(LT):socket可写变成了不可写,或者时socket不可写变成了可写

    边沿触发(ET):socket不可写变成了可写

    那么对于读写过程就要慎重处理:

    (1)读过程

    如果socket上存在可读数据,使用边沿触发就要一次读取全部可读。

    (2)写过程

    如果socket上存在可写数据,使用水平触发,一定要一次把全部写数据写完。

    上面这两点很好理解吧,因为如果在读过程中,边沿触发只进行一次,除非下来来新的数据,如果不一次读完,则会出现问题,也许是数据丢失。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. int main(){
    13. //创建套接字
    14. int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    15. if(listenfd == -1)
    16. {
    17. std::cerr << "create socket error " << std::endl;
    18. return -1;
    19. }
    20. //设置为非阻塞
    21. int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
    22. int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    23. if(fcntl(listenfd, F_SETFL, newSocketFlag)==-1)
    24. {
    25. close(listenfd);
    26. std::cerr<<"scoket flag change error" <
    27. }
    28. int no = 1;
    29. setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*) &no, sizeof(no));
    30. setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*) &no, sizeof(no));
    31. //设置服务器的地址
    32. struct sockaddr_in server_addr;
    33. memset(&server_addr, 0, sizeof(struct sockaddr_in));
    34. server_addr.sin_family = AF_INET;
    35. server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    36. server_addr.sin_port = htons(3000);
    37. //绑定服务器的地址
    38. if(bind(listenfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) == -1)
    39. {
    40. std::cout << "绑定失败" << std::endl;
    41. close(listenfd);
    42. return -1;
    43. }
    44. //监听
    45. if(listen(listenfd, 100) == -1)
    46. {
    47. close(listenfd);
    48. return -1;
    49. }
    50. //创建一个epollfd
    51. int epollfd = epoll_create(1);
    52. if(epollfd == -1)
    53. {
    54. std::cerr << "create epollfd error\n";
    55. close(listenfd);
    56. return -1;
    57. }
    58. //事件的设置
    59. epoll_event listen_fd_event;
    60. listen_fd_event.data.fd = listenfd;//设置监听对象
    61. listen_fd_event.events = EPOLLIN;//设置监听事件
    62. //默认使用的是水平触发
    63. //加入宏定义之后可以使用边沿触发
    64. #ifdef __ET__
    65. std::cout << "set ET " <
    66. listen_fd_event.events |= EPOLLET;
    67. #endif
    68. //将listenfd绑定到epollfd上面
    69. if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
    70. {
    71. std::cerr<<"epoll_ctl error\n";
    72. close(listenfd);
    73. return -1;
    74. }
    75. int n;
    76. while(true)
    77. {
    78. //检测事件,将监听的事件写入到下面这个数组中
    79. epoll_event epoll_events[1024];
    80. n = epoll_wait(epollfd, epoll_events, 1024, 1000);
    81. if(n < 0)
    82. {
    83. if(errno == EINTR)
    84. continue;
    85. std::cerr << "epoll_wait is error" << std::endl;
    86. break;
    87. }
    88. //监听超时,继续监听
    89. else if(n == 0)
    90. continue;
    91. //检测事件集合
    92. for(size_t i = 0; i < n; ++i)
    93. {
    94. //如果事件被检测到可读
    95. if(epoll_events[i].events & EPOLLIN)
    96. {
    97. if(epoll_events[i].data.fd == listenfd)//如果可读的事件是listenfd
    98. {
    99. //给连接到的客户端创建套接字,设置为非阻塞
    100. struct sockaddr_in clientaddr;
    101. memset(&clientaddr, 0, sizeof(struct sockaddr_in));
    102. socklen_t len = sizeof(struct sockaddr_in);
    103. int clientsock = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
    104. if(clientsock != -1)
    105. {
    106. int oldSocketFlag = fcntl(clientsock, F_GETFL, 0);
    107. int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    108. if(fcntl(clientsock, F_SETFL, newSocketFlag) == -1)
    109. {
    110. close(clientsock);
    111. std::cerr<<"change client socket fail\n";
    112. }
    113. else
    114. {
    115. epoll_event client_fd_event;
    116. client_fd_event.data.fd = clientsock;
    117. client_fd_event.events = EPOLLIN;
    118. #ifdef __EL__
    119. client_fd_event.events |= EPOLLET;
    120. #endif
    121. if(epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &client_fd_event))
    122. {
    123. std::cout << "new client socket create successful" << std::endl;
    124. }
    125. else
    126. {
    127. std::cerr << "client epoll ctl is error " << std::endl;
    128. close(clientsock);
    129. }
    130. }
    131. }
    132. else//其他事件可读,每次读取一个字节。用来测试水平触发
    133. {
    134. char recvBuf[100] = "";
    135. int m = recv(epoll_events[i].data.fd, recvBuf, 1, 0);
    136. if(m == 0)
    137. {
    138. //对端关闭了连接,从epollfd上移除clientfd
    139. if(epoll_ctl(epollfd,EPOLL_CTL_DEL,epoll_events[i].data.fd, NULL))
    140. {
    141. std::cout << "客户端断开连接" << epoll_events[i].data.fd << std::endl;
    142. }
    143. close(epoll_events[i].data.fd);
    144. }
    145. else if(m < 0)
    146. {
    147. //如果出错
    148. if(errno != EINTR && errno != EWOULDBLOCK)
    149. {
    150. if(epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL)!=-1)
    151. {
    152. std::cout << "客户端断开连接" << epoll_events[i].data.fd << std::endl;
    153. }
    154. close(epoll_events[i].data.fd);
    155. }
    156. }
    157. else
    158. {
    159. std::cout << "recv form client: " << recvBuf << ", clientfd: "<< epoll_events[i].data.fd << std::endl;
    160. }
    161. }
    162. }
    163. else if(epoll_events[i].events & EPOLLERR)
    164. {
    165. std::cerr<< "events is error\n";
    166. }
    167. }
    168. }
    169. close(listenfd);
    170. return 0;
    171. }
     

    编译的时候可以加入不同的宏定义来测试,可以得知使用水平触发的时候,只要缓存区有数据,就会被不断读取。我们方式一个字符串到服务器,现在服务器每次只能读取一个字符,会发现水平触发的时候,它每次都读取一个字符,直到读取完毕,而加入宏定义__ET__之后,发送一个长字符串,它读取一个字符后就不动了,不读取了。

    同样的,我们修改一下事件触发的方式,可以看看写模式下的两种触发方式

    在这种写模式下,水平触发会不断发送内核缓存可写的信号,导致程序疯狂输出。

    总结:在ET模式下,服务器给客服端注册可写模式后,在经过一次触发后,除非新的数据到达,否则不会被触发,如果需要在缓存区有数据的时候再次触发,则需要再次注册,检测可写事件。在ET模式下,读事件时必须把数据读取干净,否则下一次事件发送过来,就没有机会读上一次没读完的数据了。在LT模式下,不需要写事件的时候,一定要移除对他的注册

    4.EPOLLONESSHOT选项

    如果epoll模型注册了这个模式,则被监听的对象在触发一次之后,再也不会被触发

    8.readv和writev

    高效的服务要减少系统调用,而write和read都是系统调用,他们读取文件描述符对应的缓冲区,然后将缓冲区的数据写入到其他地方,这样就需要频繁进行系统调用,我们也可以一次性的把其他缓冲区的内容放在一起,按照自定义的规则进行一次系统调用。这个方法就是如下几个函数:

    ssize_t readv(int fd, const struct iovec* iov, int iovcnt);

    ssize_t writev(int fd, const struct iovec* iov, int iovcnt);

    ssize_t preadv(int fd, const struct iovec* iov, int iovcnt, off_t offset);

    ssize_t pwritev(int fd, const struct iovec* iov, int iovcnt, off_t offset);

    fd代表文件描述符

    offset代表平移指针位置,也就是从某个地方开始读取或者写入

    iov是一个结构体指针,用来指向一个结构体数据,它是用来连接多个需要一起连接到fd的对象组

    iovcnt代表数组元素个数.

    返回值为读取或者写入的字节数

    struct iovcnt{

            void* iov_base;//数据起始位置

            size_t iov_len;//需要传输的字节数

    };

    1. char *buf1[] = "第一个缓存区11111111111111";
    2. char *buf2[] = "第二个缓冲区22222222222222";
    3. char *buf3[] = "第三个缓冲区33333333333333";
    4. struct iovec iov[3];
    5. iov[0].iov_base = buf1;
    6. iov[0].iov_len = strlen(buf1);
    7. iov[1].iov_base = buf2;
    8. iov[1].iov_len = strlen(buf2);
    9. iov[2].iov_base = buf3;
    10. iov[2].iov_len = strlen(buf3);
    11. size_t nwirtelen = writev(fpCSV, iov, 2);

    9.本机字节序和网络字节序

    本机字节序分为小端和大端两种,小端指的是一个整数,它的高位被储存在高位地址,低位储存在低位地址,大端相反。网络字节序是网络传输过程使用的字节序,他用的是大端。

    给一个例子

    比如一个数字位0x1234

    小端字节序上,如果我们将他转换为char类型,然后作为整数输出,得到的是12

    大端字节序就是34

  • 相关阅读:
    el-tab-pane遇到的问题记录闲谈
    金仓数据库KingbaseES服务器应用参考手册--13. kingbase
    MongoDB增删改查常用操作以及podman下载MongoDB命令
    LSTM-Attention单维时间序列预测研究(Matlab代码实现)
    腾讯云新客户优惠服务器88元/年,540元/3年,另有5年优惠服务器
    从 MMU 看内存管理
    1 认识 Android
    java虚拟机详解篇十二(方法调用和方法的绑定机制)
    每日杂学:页面加载出现白页
    使用g2opy 做一个简单的二维回环优化 Slam(附python代码)
  • 原文地址:https://blog.csdn.net/weixin_42581560/article/details/133788580