目录
将套接字设置为非阻塞的
1.使用socket函数创建的套接字,将其设置为非阻塞的
- 使用
socket
函数创建套接字时,指定SOCK_NONBLOCK标志位
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (listenfd == -1) {
exit(-1);
}
- 使用fcntl函数实现
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (listenfd == -1) {
exit(-1);
}
int flags = fcntl(listenfd, F_GETFD, 0);
flags |= O_NONBLOCK;
fcntl(listenfd, F_SETFD, flags);
2.服务端开发中,将用于通信的套接字设置为非阻塞的
- 在接收连接请求时使用accept4函数
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept4(listenfd, (struct sockaddr*)&client_addr, &client_addr_len, SOCK_NONBLOCK);
- 在接收连接请求时使用accept函数,然后使用fcntl函数将用于通信的套接字设置为非阻塞的。
实现字节序转换函数
1.字节序
- 大端序:又称网络字节序。数据的高位字节存储在内存低地址
- 小端序:又称主机字节序。数据的高位字节存储在内存高地址
2.字节序的判断
#include <stdio.h>
union
{
char c[4];
unsigned int n;
}host_order = {'0','1','2','3'};
#define LITTLE_ENDIAN ('0' == (char)host_order.n)
#define BIG_ENDIAN ('3' == (char)host_order.n)
int main() {
// 0x33323130
printf("%#x\n",host_order.n);
if (LITTLE_ENDIAN) {
printf("本主机使用的字节序为小端字节序\n");
} else if (BIG_ENDIAN) {
printf("本主机使用的字节序为大端序\n");
}
return 0;
}
3.字节序转换
- 16位整数从主机字节序转换为网络字节序
// 将16位的端口号从主机字节序转换为网络字节序
unsigned short int m_htons(unsigned short int port) {
if LITTLE_ENDIAN {
return port << 8 | port >> 8;
}
return port;
}
- 将32位的整数从主机字节序转换为网络字节序
// 将32位的IP从主机字节序转换为网络字节序
unsigned int m_htonl(unsigned int ip) {
if LITTLE_ENDIAN {
return (ip << 24) |
(ip << 8 & 0x00ff0000) |
(ip >> 8 & 0x0000ff00) |
(ip >> 24);
}
return ip;
}
非阻塞的connect
- 使用非阻塞的套接字向服务端发起连接会理解返回,此时需要利用一些方法得到connect函数调用的返回结果
- 方法1:
- 将套接字设置为非阻塞的
- 判断connect函数的返回值,如EINTR、EINPROGRESS。如果返回0则表示连接成功,直接返回,无34两部。
- 如果错误码为EINPROGRESS,则使用select进一步判断是否可写来判断连接是否成功建立。
- 使用getsockopt判断是否出错,不出错则表示由连接成功触发的可写事件,从而表示连接成功建立
#include <arpa/inet.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/select.h> #define SEVER_ADDRESS "127.0.0.1" #define SERVER_PORT 8000 bool connect(int timeout = 6) { // 创建非阻塞的套接字 int clientfd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); if (clientfd == -1) { perror("create socket error"); return false; } struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); serv_addr.sin_addr.s_addr = inet_addr(SEVER_ADDRESS); while (1) { int ret = connect(clientfd, (sockaddr*)&serv_addr, sizeof(serv_addr)); // 连接成功 if (ret == 0) { close(clientfd); return true; } else if (ret == -1) { // 被信号中断 if (errno == EINTR) { printf("连接过程中被信号中断...\n"); continue; } else if (errno == EINPROGRESS) { printf("连接正在进行中...\n"); break; } else { close(clientfd); return false; } } } fd_set writefds; FD_ZERO(&writefds); FD_SET(clientfd, &writefds); struct timeval tv; tv.tv_sec = timeout; tv.tv_usec = 0; if (select(clientfd + 1, nullptr, &writefds, nullptr, &tv) <= 0) { perror("连接失败"); close(clientfd); return false; } // clientfd上写事件就绪 int err; socklen_t len = static_cast<socklen_t>(sizeof err); //调用getsockopt检测此时socket是否出错 if (getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0 || err != 0) { close(clientfd); return false; } // 无错则表示连接成功触发的写事件就绪 if (err == 0) { printf("连接成功\n"); close(clientfd); return true; } else perror("连接失败"); close(clientfd); return false; } int main() { bool flag = connect(); printf("flag:%s\n", flag == true ? "连接成功" : "连接失败"); return 0; }
- 方法2:使用poll代替方法1中的select
常见的socket选项
- SO_LINGER选项
- 这个选项用于控制close系统调用在关闭TCP连接时的行为。可以通过调用setsockopt函数设置SO_LINGER选项的值,示例如下:
#include <sys/socket.h> struct linger { int l_onoff; // 0表示关闭该选项,非0表示开启该选项 int l_linger; // 滞留时间 }
- close系统调用可能产生的不同的行为:
- TCP_NODELAY:这个选项与Nagle算法有关,设置了这个选项表示禁止Nagle算法。
- SO_KEEPALIVE:发送周期性保活报文以维持连接。套接字设置了这个选项,将默认每隔两个小时发送一个心跳检测包给对端。两个小时太长,可以通过设置TCP_KEEPIDLE、TCP_KEEPINTVL 和 TCP_KEEPCNT选项进行更改。示例代码如下:
// 设置SO_KEEPALIVE选项
int val = 1
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val));
//发送 keepalive 报文的时间间隔
val = 7200;
// IPPROTO_TCP表示TCP选项
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val));
//两次重传报文的时间间隔
int interval = 75;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
// 重传次数
int cnt = 9;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
- SO_RCVBUF/SO_SNDBUF:TCP接收缓冲区和发送缓冲区大小
- 设置超时时间的选项
- SO_RCVTIMO:对于阻塞套接字,设置接收数据阻塞时的时长
- SO_SNDTIMEO:对于阻塞套接字,设置发送数据阻塞时的时长或者connect系统调用阻塞的时长。对于connect系统调用,系统调用超时后返回-1,错误码为EINPROGRESS。示例如下:
int timeout_connect(const char* ip, int port, int timeout) { int ret = 0; struct sockaddr_in address; bzero(&address, sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET, ip, &address.sin_addr); address.sin_port = htons(port); int sockfd = socket(PF_INET, SOCK_STREAM, 0); struct timeval time; time.tv_sec = timeout; time.tv_usec = 0; socklen_t len = sizeof(time); setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &time, len); ret = connect(sockfd, (const sockaddr*)&address, sizeof(address)); if (ret == -1) { if (errno == EINPROGRESS) { printf("连接超时\n"); // 这里可以执行一些定时任务 return -1; } printf("错误发生\n"); return -1; } return sockfd; }
- SO_REUSEADDR/SO_REUSEPORT:分别设置IP地址或者端口的复用
- SO_ERROR:套接字上的错误
bind函数
- 在编写服务端时,通过bind函数绑定0IP地址。系统将会选择一个网卡地址
- 在编写服务端时,通过bind函数绑定0号端口。系统将会选择一个端口号
- 在编写客户端时,调用bind函数绑定指定的端口与服务端通信。客户端将以指定的端口号运行。
listen函数
- listen函数的第二参数backlog:表示未接受处理请求连接的最大长度,pending connections将放在一个队列上,这个backlog参数即为这个队列的长度
服务端如何保证在关闭连接前将数据包发送给客户端
这是一个半关闭问题
- 调用send/write发送数据
- 设置SO_LINGER选项
- 启动TCP_NODELAY选项
- 调用shutdown函数
epoll模型的触发方式
监听套接字是用于监听客户端连接请求的套接字,通信套接字是用于和客户端数据通信的套接字。epoll模型的触发方式有如下几种:
- 侦听套接字是非阻塞的水平触发模式:这种情况可行
- 通信套接字是阻塞的水平触发模式:这种情况与通信套接字是非阻塞的水平触发模式效果一样。
- 通信套接字是非阻塞的水平触发模式:这种情况可行
- 侦听套接字是非阻塞的边缘触发模式:这种情况不可行,可能会出现部分客户端连接不上的情况
- 通信套接字是阻塞的边缘触发模式:这种情况不可行,因为通信套接字是阻塞的,则在收发数据时会阻塞网络线程,导致无法处理新的客户端的连接请求。
- 通信套接字是非阻塞的边缘触发模式:这种情况下可行,需要在循环中收取数据。
总结:epoll的水平触发模式既可以使用阻塞IO,也可以使用非阻塞IO;边缘触发模式只能使用非阻塞io,即将用于和客户端通信的套接字设置成非阻塞的。