alin的学习之路:基于Windows的套接字的使用、阻塞超时的处理
1. 基于window套接字通信
在编写Qt程序的时候, 可以使用第三方库, 可以调用操作系统的API(添加对应的头文件即可), 使用标准c/c++函数或者库都是可以的。
使用windows中的套接字函数
- 使用包含的头文件
include <winsock2.h>
—> 这是window是中的一个套接字通信的头文件- 使用的套接字库
ws2_32.dll
----> window自带的套接字通信的动态库在Qt中使用方式:
找到项目文件
*.pro
在这个文件中, 尾部添加一句话, 指定库的名字
LIBS += -lws2_32
1.1 初始化套接字环境
// 初始化Winsock库
// 返回值: 成功返回0,失败返回SOCKET_ERROR。
WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
参数:
- wVersionRequested: 使用的Windows Socket的版本, 一般使用的版本是 2.2
- 初始化这个参数: MAKEWORD(2, 2);
- lpWSAData:一个WSADATA结构指针, 这是一个传入参数
- 创建一个 WSADATA 类型的变量, 将地址传递给该函数的第二个参数
// 注销Winsock相关库
// 返回值:成功返回0,失败返回SOCKET_ERROR。
int WSACleanup (void);
举例
WSAData wsa;
// 初始化套接字库
WSAStartup(MAKEWORD(2, 2), &wsa);
// .......
// 注销Winsock相关库
WSACleanup();
1.2 套接字通信函数
基于Linux的套接字通信流程是最全面的一套通信流程, 如果是在某个框架中进行套接字通信, 通信流程只会更简单, 直接使用window的套接字api进行套接字通信, 和Linux平台上的通信流程完全相同。
1.2.1 结构体
///
/// Windows ///
///
typedef struct in_addr {
union {
struct{ unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;
struct{ unsigned short s_w1, s_w2;} S_un_w;
unsigned long S_addr; // 存储IP地址
} S_un;
}IN_ADDR;
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
///
Linux
///
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
struct in_addr
{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
1.2.2 大小端转换函数
// 主机字节序 -> 网络字节序
u_short htons (u_short hostshort );
u_long htonl ( u_long hostlong);
// 网络字节序 -> 主机字节序
u_short ntohs (u_short netshort );
u_long ntohl ( u_long netlong);
// linux函数, window上没有这两个函数
inet_ntop();
inet_pton();
// windows 和 linux 都使用, 只能处理ipv4的ip地址
// 点分十进制IP -> 大端整形
unsigned long inet_addr (const char FAR * cp); // windows
in_addr_t inet_addr (const char *cp); // linux
// 大端整形 -> 点分十进制IP
// window, linux相同
char* inet_ntoa(struct in_addr in);
1.2.3 套接字函数
window的api中套接字对应的类型是 SOCKET 类型, linux中是 int 类型, 本质是一样的
// 创建一个套接字
// 返回值: 成功返回套接字, 失败返回INVALID_SOCKET
SOCKET socket(int af,int type,int protocal);
参数:
- af: 地址族协议
- ipv4: AF_INET (windows/linux)
- PF_INET (windows)
- AF_INET == PF_INET
- type: 和linux一样
- SOCK_STREAM
- SOCK_DGRAM
- protocal: 一般写0 即可
- 在windows上的另一种写法
- IPPROTO_TCP, 使用指定的流式协议中的tcp协议
- IPPROTO_UDP, 使用指定的报式协议中的udp协议
// 关键字: FAR NEAR, 这两个关键字在32/64位机上是没有意义的, 指定的内存的寻址方式
// 套接字绑定本地IP和端口
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int bind(SOCKET s,const struct sockaddr FAR* name, int namelen);
// 设置监听
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int listen(SOCKET s,int backlog);
// 等待并接受客户端连接
// 返回值: 成功返回用于的套接字,失败返回INVALID_SOCKET。
SOCKET accept ( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );
// 连接服务器
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int connect (SOCKET s,const struct sockaddr FAR* name,int namelen );
// 在Qt中connect用户信号槽的连接, 如果要使用windows api 中的 connect 需要在函数名前加::
::connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 接收数据
// 返回值: 成功时返回接收的字节数,收到EOF时为0,失败时返回SOCKET_ERROR。
// ==0 代表对方已经断开了连接
int recv (SOCKET s,char FAR* buf,int len,int flags);
// 发送数据
// 返回值: 成功返回传输字节数,失败返回SOCKET_ERROR。
int send (SOCKET s,const char FAR * buf, int len,int flags);
// 关闭套接字
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int closesocket (SOCKET s); // 在linux中使用的函数是: int close(int fd);
//----------------------- udp 通信函数 -------------------------
// 接收数据
int recvfrom(SOCKET s,char FAR *buf,int len,int flags,
struct sockaddr FAR *from,int FAR *fromlen);
// 发送数据
int sendto(SOCKET s,const char FAR *buf,int len,int flags,
const struct sockaddr FAR *to,int tolen);
1.2.4 IO多路转接函数
IO多路转接函数中, 只支持
select
epoll和poll是仅支持Linux下的
struct timeval
{
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微妙
};
// 函数是阻塞函数, 阻塞的最大时长通过最后一个参数控制
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, const struct timeval* timeout);
参数:
- nfds:
windows: 本参数忽略,仅起到兼容作用,设为0即可;
linux: 检测的集合中最大的文件描述符 + 1
- readfds:读集合, 检测集合中套接字的读事件;
- writefds:写集合, 检测集合中套接字的写事件;
- 检测能不能写, 是否具备写的条件
- exceptfds:异常集合, 检测集合中套接字的异常事件;
- timeout:本函数最多等待时间,对阻塞操作则为NULL。
返回值:
- 成功: 集合中满足条件的套接字总个数
- 超时: 返回0
- 失败: 返回SOCKET_ERROR错误
void FD_CLR(SOCKET s,fd_set *set); // 从集合set中删除套接字s。
int FD_ISSET(SOCKET s,fd_set *set); // 若s为集合中一员,返回0;否则返回非0值。
void FD_SET(SOCKET s,fd_set *set); // 向集合添加套接字s。
void FD_ZERO(fd_set *set); // 将set初始化为空集NULL。
1.2.5 setsockopt
这是一个多功能函数: 设置套接字属性:
- 使用比较多的场景: 在tcp通信的服务器端, 设置端口复用
// linux
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
// windows
#include <winsock2.h>
int setsockopt(SOCKET sockfd, int level, int optname,
const char *optval, int optlen);
1.2.6 ioctlsocket
// 在linux中进行文件描述符重定向: dup, dup2, fcntl --> linux api
// fcntl -> 修改文件描述符的flag属性, 最常用: 设置阻塞和非阻塞
// 这是windows中的一个api函数, 类似于linux中的fcntl
// 设置套接字属性
// 修改套接字阻塞非阻塞的方式
// 这是window api linux中没有这个函数
int ioctlsocket( SOCKET s, long cmd, u_long FAR *argp );
参数:
- s: 套接字
- cmd:
- FIONBIO:允许或禁止套接字s的非阻塞模式
- argp:
- 0: 阻塞模式
- 1: 非阻塞模式
2. 套接字超时处理
超时是怎么回事儿?
在使用linux/windows 套接字 api进行通信的时候, 调用了这些函数
- accept()
- recv()
- send()
- connect()
这些函数的行为默认都是阻塞的, 如果条件不满足, 这些函数就会一直阻塞(connect不算), 如果不想让这些函数一直阻塞, 就可以设置一个超时时长, 当指定的时间之后还不满足条件, 就让这些函数解除阻塞
超时处理需要程序猿自己实现, 默认是没有的
// 等待并接受客户端连接
// 返回值: 成功返回用于的套接字,失败返回INVALID_SOCKET。
// 函数阻塞: 当没有客户端连接的时候, 这个函数一直阻塞, 检测到新的连接阻塞解除, 连接建立
// 函数调用检测监听的套接字 s 在内核对应的读缓冲区中是否有数据, 这个数据就客户端发送的请求数据,
// 如果没有函数就一直处于阻塞状态, 如果有解除阻塞建立连接
SOCKET accept ( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );
// 连接服务器
// 返回值: 成功返回0,失败返回SOCKET_ERROR
// 阻塞: 连接服务器过程中(三次握手的过程)函数是阻塞的, 函数连接完成, 解除阻塞
// - 连接完成对应的结果: 连接成功, 连接失败
// - 这个函数有一默认的解除阻塞的时长, 时间长度是 75s
// - 如果连接了75s还没有成功, 强制解除连接, 返回错误号
int connect (SOCKET s,const struct sockaddr FAR* name,int namelen );
// 接收数据
// 返回值: 成功时返回接收的字节数,收到EOF时为0,失败时返回SOCKET_ERROR。
// ==0 代表对方已经断开了连接
// 阻塞: 收不到对方发送的数据就阻塞, 检测到了通信数据, 解除阻塞, 读数据
// `函数调用检测通信的套接字 s 在内核对应的读缓冲区中是否有数据, 这个数据就对端发送的通信数据,
// `如果没有函数就一直处于阻塞状态, 如果有解除阻塞, 将对方发送过来的数据读出
int recv (SOCKET s,char FAR* buf,int len,int flags);
// 发送数据
// 返回值: 成功返回传输字节数,失败返回SOCKET_ERROR。
// 阻塞: 在发送数据之前检测到套接字对应的内核缓冲区已经被写满了, 如果写缓冲区有空闲空间, 写解除阻塞
// `函数调用检测通信的套接字 s 在内核对应的写缓冲区是否可以写(缓冲区满了就不可写)
// `如果缓冲区是满的, send()函数一直阻塞, 如果写缓冲区不满了, send()解除阻塞,
// 将数据写入到内核的写缓冲区中
int send (SOCKET s,const char FAR * buf, int len,int flags);
-- 超时检测:
- 能不能使用sleep(10); --> 不行
- 阻塞时长是固定的, 不能提前解除阻塞
- sleep()没有检测文件描述符状态的能力, 在sleep()解除阻塞之后,
这些阻塞阻塞函数是不是能够被调用是无法判断出来的
- 检测只能使用IO多路转接函数
- 有检测文件描述符状态的能力
- 这些检测函数是阻塞的, 但是有超时时长的设置
- 在超时时长这个最大的阻塞时间段期间, 如果检测到了满足条件的文件描述符, 函数可以提前解除阻塞
2.1 accept 超时处理
// 可以使用 IO多路转接函数, 帮助accept() 函数检测 监听的套接字读缓冲区
// 如果在一定的时间段之内: 检测到了数据 -> 调用accept(), 如果没有检测到数据, 就不调用accept()
// 伪代码
// 创建监听的套接字
SOCKET lsock = socket();
bind();
listen();
// 等待(在某一个事件段之内)并接受客户端连接
// 委托IO多路转接函数帮助我们进行检测, 在window中只能使用select
/*
struct timeval
{
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微妙
};
void FD_CLR(SOCKET s,fd_set *set); // 从集合set中删除套接字s。
int FD_ISSET(SOCKET s,fd_set *set); // 若s为集合中一员,返回0;否则返回非0值。
void FD_SET(SOCKET s,fd_set *set); // 向集合添加套接字s。
void FD_ZERO(fd_set *set); // 将set初始化为空集NULL。
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, const struct timeval* timeout);
*/
fd_set reads;
FD_ZERO(&reads);
FD_SET(lsock, &reads); // 要检测的套接字放到检测集合中
// 开始检测
struct timeval val;
val.tv_sec = 5; // 5s
val.tv_usec = 0;
int ret = select(0, &reads, NULL, NULL, &val); // 超时时长为5秒, 最长检测5s
// 判断返回值
if(ret == 1)
{
// 有新的连接请求, 处理这个请求, 和客户端建立新的连接
SOCKET conn = accept(lsock, NULL, NULL);
}
else if(ret == 0)
{
// 在5s之内没有检测到数据(读缓冲区), 超时到达, 强制返回
// 做其他处理, 不调用accept()
}
else
{
// 返回值为 -1, 异常处理
}
2.2 recv 超时处理
// 可以使用 IO多路转接函数, 帮助 recv() 函数检测 通信的套接字读缓冲区
// 如果在一定的时间段之内: 检测到了数据 -> 调用 recv(), 如果没有检测到数据, 就不调用 recv()
// 伪代码 --> 基于服务器进行处理(客户端也有通信的文件套接字)
// 创建监听的套接字
SOCKET lsock = socket();
bind();
listen();
SOCKET sock = accept(lsock, NULL, NULL);
//
// 等待(在某一个事件段之内)并接受客户端连接
// 委托IO多路转接函数帮助我们进行检测, 在window中只能使用select
/*
struct timeval
{
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微妙
};
void FD_CLR(SOCKET s,fd_set *set); // 从集合set中删除套接字s。
int FD_ISSET(SOCKET s,fd_set *set); // 若s为集合中一员,返回0;否则返回非0值。
void FD_SET(SOCKET s,fd_set *set); // 向集合添加套接字s。
void FD_ZERO(fd_set *set); // 将set初始化为空集NULL。
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, const struct timeval* timeout);
*/
fd_set reads;
FD_ZERO(&reads);
FD_SET(sock, &reads); // 要检测的套接字放到检测集合中 -> 通信的套接字
// 开始检测
struct timeval val;
val.tv_sec = 5; // 5s
val.tv_usec = 0;
int ret = select(0, &reads, NULL, NULL, &val); // 超时时长为5秒, 最长检测5s
// 判断返回值
if(ret == 1)
{
// 检测到了对方发送的数据, 接收数据
char buf[1024];
int len = recv(sock, buf, sizeof(buf), 0);
}
else if(ret == 0)
{
// 在5s之内没有检测到数据(读缓冲区), 超时到达, 强制返回
// 做其他处理, 不调用 recv()
}
else
{
// 返回值为 -1, 异常处理
}
2.3 send() 超时处理
// 可以使用 IO多路转接函数, 帮助 send() 函数检测 通信的套接字写缓冲区
// 如果在一定的时间段之内:
// - 写缓冲区可写(有存储空间) -> 调用 send()
// - 写缓冲区不可写(没有存储空间了) -> 就不调用 send()
// 伪代码 --> 基于服务器进行处理(客户端也有通信的文件套接字)
// 创建监听的套接字
SOCKET lsock = socket();
bind();
listen();
SOCKET sock = accept(lsock, NULL, NULL);
//
// 等待(在某一个事件段之内)并接受客户端连接
// 委托IO多路转接函数帮助我们进行检测, 在window中只能使用select
/*
struct timeval
{
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微妙
};
void FD_CLR(SOCKET s,fd_set *set); // 从集合set中删除套接字s。
int FD_ISSET(SOCKET s,fd_set *set); // 若s为集合中一员,返回0;否则返回非0值。
void FD_SET(SOCKET s,fd_set *set); // 向集合添加套接字s。
void FD_ZERO(fd_set *set); // 将set初始化为空集NULL。
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, const struct timeval* timeout);
*/
fd_set writes;
FD_ZERO(&writes);
FD_SET(sock, &writes); // 要检测的套接字放到检测集合中 -> 通信的套接字
// 开始检测
struct timeval val;
val.tv_sec = 5; // 5s
val.tv_usec = 0;
int ret = select(0, NULL, &writes, NULL, &val); // 超时时长为5秒, 最长检测5s
// 判断返回值
if(ret == 1)
{
// 检测到写缓冲区可用, 发送数据(将要发送的数据写入到套接字对应的写缓冲区)
char *buf = "hello, world!";
int len = send(sock, buf, strlen(buf), 0);
}
else if(ret == 0)
{
// 写缓冲区已经被写满了, 不能再写了, 不能调用send()函数
// 其他处理
}
else
{
// 返回值为 -1, 异常处理
}
2.4 connect 超时处理
connect() 调用由客户端发起, 只要一调用就阻塞
- 阻塞期间是无法处理的
- 阻塞解除之后再处理就晚了
- 应该在connect()调用之后, 三次握手期间进行检测, 检测指定的时间长度, 如果还没有成功, 就认为失败了
- 默认这个事儿没法做, 搞不定
- 必须要将这个函数修改为非阻塞 的才可以在函数执行期间进行检测
如何设置非阻塞?
- 在linxu上使用函数
fcntl
, 在window上可以使用函数:ioctlsocket
需要了解conenct()在非阻塞状态下的相关属性
默认情况下,
connect() 函数自带超时处理机制的, 默认超时时间是75s
, 也就是这个函数最多阻塞75s如果想要修改默认的阻塞时长, 需要额外处理, 不能修改函数内部的源码
如果将connect() 设置为非阻塞, 就有以下属性:
连接过程中通信的套接字的写缓冲区不可用
连接成功了, 套接字可读可写
连接失败了, 套接字可读可写
- 得到的结论:
- 通过上述属性描述, 可以通过通信的套接字的写缓冲区的状态判断连接过程是不是完成了
- 写缓冲区从
不可写 -> 可写,
说明连接过程完毕了
- 不能够判断出连接最后的状态是成功了还是失败了
- 如果想要知道连接最终的状态, 需要通过函数
getsockopt()
对套接字进行状态的判断
// 可以使用 IO多路转接函数, 帮助 connect() 函数检测 通信的套接字写缓冲区
// 如果在一定的时间段之内:
// - 写缓冲区可写 -> 连接已经完成(不清楚成功还是失败)
// - 写缓冲区不可写 -> 连接还在进行中...
// 伪代码 --> 客户端代码
// 创建通信的套接字
SOCKET sock = socket();
struct sockaddr_in serverAddr;
// init serverAddr
// ....
// 设置通信的套接字的非阻塞(windows)
u_long status = 1;
ioctlsocket(sock, FIONBIO, &status);
// 连接服务器 -> 连接过程是非阻塞
SOCKET sock = connect(sock, &serverAddr, sizeof(serverAddr));
//
// 等待(在某一个事件段之内)并接受客户端连接
// 委托IO多路转接函数帮助我们进行检测, 在window中只能使用select
/*
struct timeval
{
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微妙
};
void FD_CLR(SOCKET s,fd_set *set); // 从集合set中删除套接字s。
int FD_ISSET(SOCKET s,fd_set *set); // 若s为集合中一员,返回0;否则返回非0值。
void FD_SET(SOCKET s,fd_set *set); // 向集合添加套接字s。
void FD_ZERO(fd_set *set); // 将set初始化为空集NULL。
int select(int nfds, fd_set* readfds, fd_set* writefds,
fd_set* exceptfds, const struct timeval* timeout);
*/
fd_set writes;
FD_ZERO(&writes);
FD_SET(sock, &writes); // 要检测的套接字放到检测集合中 -> 通信的套接字
// 开始检测
struct timeval val;
val.tv_sec = 5; // 5s
val.tv_usec = 0;
int ret = select(0, NULL, &writes, NULL, &val); // 超时时长为5秒, 最长检测5s
// 判断返回值
if(ret == 1)
{
// 写缓冲区可写了, 连接完成了, 需要进一步判断成功了还是失败了
// 通过函数 getsockopt() 进行判断
int len, opt;
getsockopt(sock, SOL_SOCKET, SO_ERROR, &opt, &len);
if(opt == 0)
{
// 连接服务器成功了, 可以继续通信了
}
else
{
// 连接服务器失败了, 不能继续通信, 连接失败的处理动作
}
}
else if(ret == 0)
{
// 写缓冲区一直不可写, 连接还在继续
// 其他处理
}
else
{
// 返回值为 -1, 异常处理
}
// getsockopt() 函数原型
// 如果通过这个函数判断套接字的连接状态
int getsockopt(SOCKET sockfd, int level, int optname,
void *optval, int *optlen);
参数:
- sockfd: 通信的套接字, 判断的就是这个套接字的状态
- level: SOL_SOCKET
- optname: SO_ERROR
- optval: 这是一个传出参数, 应该将其作为一个整数来使用, 里边记录了最终的检测状态
- 如果这个整数值为0 -> 没有错误
- 这个整数值是非0 -> 有错误
- optlen: 描述的是 optval 参数占用的内存大小