目录
步骤分解
-
服务端建立
步骤说明 涉及函数 建立服务端SOCKET socket 绑定服务IP和端口 bind 监听网络端口 listen 等待建立连接 accept 发送数据 send 关闭服务端SOCKET closesocket -
客户端建立
步骤说明 涉及函数 建立客户端SOCKET socket 连接服务器 connect 接收数据 recv 关闭服务端SOCKET closesocket
搭建SOCKET开发环境
开发环境说明
window下进行SOCKET编程依赖两个库如下所示:
- 较早的DLL是
wsock32.dll
,对应的头文件为winsock1.h
; - 最新的DLL是
ws2_32.dll
,对应的头文件为winsock2.h
。
基本所有的window操作系统都已经支持了ws2_32.dll
,因此可以直接使用这个库无需考虑第一个比较早的库。除了导入头文件之外还需要导入ws2_32.lib
链接库,可使用显示导入方式
#pragma comment (lib, "ws2_32.lib")
如果在vs开发环境下也可以在项目属性中添加依赖库
,如果使用Clion则在CMakeLists中add_executable
语句前增加一条语句
link_libraries(ws2_32)
启动SOCKET说明
SOCKET标准的启动过程如下:
#include <iostream>
#include <WinSock2.h>
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// ......
WSACleanup();
return 0;
}
使用MAKEWORD(2, 2)
来明确SOCKET的版本号,WinSock规范的最新版本号为2.2
版本,wsock32.dll
仅支持1.0
和1.1
,wsock32.dll
已经能够很好的支持TCP/IP
通信程序的开发,ws2_32.dll
主要增加了对其他协议的支持,建议使用最新的2.2
版本。
如果当前工程中还需要用到window.h
头文件,需要在引入任何头文件之前增加WIN32_LEAN_AND_MEAN
宏定义,减少对较早SOCKET版本的依赖,如果能保证window.h
在WinSock2.h
之后引入也可不添加次宏定义。
创建TCP服务器
创建SOCKET
原型
int socket(int af, int type, int protocol);
参数说明:
字段 | 说明 | 参数填写 |
---|---|---|
af | 地址族,使用IP的类型 | AF_INET = IPv4,AF_INET6 = IPv6 |
type | 数据传输方式 | SOCK_STREAM = 面向连接,SOCK_DGRAM = 面向连接 |
protocol | 传输协议 | IPPROTO_TCP,IPPTOTO_UDP |
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
SOCKET server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
WSACleanup();
return 0;
}
绑定端口
原型
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);
参数说明:
字段 | 说明 | 参数填写 |
---|---|---|
sock | SOCKET对象 | 使用socket创建的对象后返回的对象 |
addr | SOCKET地址结构体 | 外部创建好并填写,IP、端口等信息 |
addrlen | SOCKET地址结构体长度 | - |
结构体定义:
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// ......
绑定地址和端口
SOCKADDR_IN sock_add_in;
sock_add_in.sin_family = AF_INET;
sock_add_in.sin_port = htons(9090);
sock_add_in.sin_addr.S_un.S_addr = INADDR_ANY;
int bind_result = bind(server_socket, (SOCKADDR*)&sock_add_in, sizeof(SOCKADDR_IN));
if (bind_result == SOCKET_ERROR) {
std::cout << "bind error." << std::endl;
}
WSACleanup();
return 0;
}
其中INADDR_ANY
表示当前服务器可以接收任意客户端请求,也可填写具体IP地址,地址填写的时候使用inet_addr
函数进行转换。端口号的填写也是一样的需要使用htons
函数进行转换。
在进行绑定时需要将SOCKADDR_IN
强转换成SOCKADDR
结构体,这两个结构体的长度是相同的都是16字节,可以简单的认为SOCKADDR
是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而SOCKADDR_IN
是专门用来保存IPv4
地址的结构体。另外还有SOCKADDR_IN6_LH
是专门用来保存IPv6
地址的结构体。
监听连接
原型
int listen(SOCKET sock, int backlog);
参数说明:
字段 | 说明 | 参数填写 |
---|---|---|
sock | SOCKET对象 | 使用socket创建的对象后返回的对象 |
backlog | 请求队列的最大长度 | - |
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// ......
监听连接
int listen_result = listen(server_socket, 5);
if (listen_result == SOCKET_ERROR) {
std::cout << "listen error." << std::endl;
}
WSACleanup();
return 0;
}
listen
使SOCKET进入监听状态,其中backlog为请求队列的最大长度
。动监听
是指当没有客户端请求时,SOCKET处于睡眠
状态,只有当接收到客户端请求时,SOCKET才会被唤醒
来响应请求。
当SOCKET正在处理客户端请求时,如果有新的请求进来
SOCKET是没法处理,只能把它放进缓冲区
,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
缓冲区的长度(即,能存放多少个客户端请求)
可以通过backlog参数指定,可以根据需求来定,并发量小的话可以是10或者20,或者将backlog的值设置为SOMAXCONN
由系统来决定请求队列长度,这个值一般比较大可能是几百或者更多。
当请求队列满时,就不再接收新的请求,客户端会收到WSAECONNREFUSED
错误。
连接客户端
原型
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
参数说明:
字段 | 说明 | 参数填写 |
---|---|---|
sock | SOCKET对象 | 使用socket创建的对象后返回的对象 |
addr | 客户端SOCKET地址结构体 | 空结构体在接收连接请求的时候填写填充客户端信息 |
addrlen | 客户端SOCKET地址结构体 | 客户端SOCKET地址结构体的长度 |
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// ......
处理连接
SOCKADDR_IN client_add_in = { 0 };
int client_lent = sizeof(SOCKADDR_IN);
SOCKET client_socket = INVALID_SOCKET;
client_socket = accept(server_socket, (SOCKADDR*)&client_add_in, &client_lent);
if (client_socket == INVALID_SOCKET) {
std::cout << "client connect error." << std::endl;
}
WSACleanup();
return 0;
}
listen只是让SOCKET进入监听状态并没有真正接收客户端请求,listen后面的代码会继续执行,accept才是处理请求的函数,accept会阻塞当前线程执行。
accept返回一个新的SOCKET
来和客户端通信,其中SOCKADDR_IN结构会被填充
,描述了客户端的IP地址
和端口号
。
向客户端发送数据
原型
int send(SOCKET sock, const char * buf, int len, int flags);
参数说明:
字段 | 说明 | 参数填写 |
---|---|---|
sock | SOCKET对象 | 使用accept后返回的SOCKET对象 |
buf | 发送数据包缓存地址 | 发送的实际数据地址 |
len | 发送的数据的字节数 | - |
flags | 为发送数据时的选项 | 一般填0 |
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// .....
发送数据
char message[] = "Build TCP Connect.";
send(client_socket, message, strnlen_s(message, 100), 0);
WSACleanup();
return 0;
}
关闭服务器
原型
closesocket(SOCKET s);
参数说明:
字段 | 说明 | 参数填写 |
---|---|---|
s | SOCKET对象 | 使用socket创建的对象后返回的对象 |
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// ......
关闭服务器
closesocket(server_socket);
WSACleanup();
return 0;
}
创建TCP客户端
创建SOCKET
客户端创建SOCKET的步骤与服务器创建SOCKET步骤一致。
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
SOCKET client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
WSACleanup();
return 0;
}
连接服务器
原型
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);
参数和bind
一样,不同的是客户端连接的时候需要指定服务器的具体IP。
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// ......
连接服务器
SOCKADDR_IN sock_add_in;
sock_add_in.sin_family = AF_INET;
sock_add_in.sin_port = htons(9090);
sock_add_in.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int connect_result = connect(client_socket, (sockaddr*)&sock_add_in, sizeof(SOCKADDR_IN));
if (connect_result == SOCKET_ERROR) {
std::cout << "connect error." << std::endl;
}
WSACleanup();
return 0;
}
接收数据
原型
int recv(SOCKET sock, char *buf, int len, int flags);
参数说明:
字段 | 说明 | 参数填写 |
---|---|---|
sock | SOCKET对象 | 使用socket创建的SOCKET对象 |
buf | 接收数据包缓存地址 | 接收到数据后存放的内存地址 |
len | 数据缓存区的大小 | - |
flags | 为发送数据时的选项 | 一般填0 |
使用
int main(int argc, char * argv[]) {
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
// ......
接收数据
int recv_len = recv(client_socket, message, 256, 0);
if (recv_len > 0) {
std::cout << "recv message : %s" << message << std::endl;
}
WSACleanup();
return 0;
}
recv返回实际接收到的数据包的大小,可以根据返回值判断数据是否正确,最后关闭socket即可。