Winsock编程记录
Winsock就是windows下的socket编程的简称,函数用法与BSD的socket(即unix中的socket)编程基本相同。
所有的代码实现位于我的Github。
重要:所有的函数使用、讲解和实例请参考Winsock文档 via Microsoft。
环境
使用VS开发,需要使用的头文件和库文件如下
// 头文件
#include <winsock2.h>
#include <ws2tcpip.h>
// 静态库
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")
基本数据结构
使用比较多的数据结构包括sockaddr,addrinfo等,定义如下。
struct sockaddr {
ushort sa_family;
char sa_data[14];
};
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
typedef struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
} ADDRINFOA, *PADDRINFOA;
这里的sockaddr_in为IP协议的地址结构体,sockaddr为所有网络层协议的地址结构体,可以保存所有网络层协议的地址数据。当使用IP协议栈时使用sockaddr_in结构体,winsock的函数大多使用sockaddr作为函数参数,从而实现更好的兼容性。(winsock还实现了sockaddr_storage,地址空间更大,兼容性更好)。
基本函数模块
由于Winsock有完备的错误代码提示,因此养成在每一步操作之后都要检查错误代码的习惯。这里添加一个全局的函数返回值(错误代码)变量用于检测操作是否完成。
int iResult = 0;
Winsock初始化:WSAStartup()
Winsock在socket各种函数的基础上实现了一套WSA函数(WinSockApplication),需要在程序前部初始化。
WORD wVersion = MAKEWORD(2, 2);
WSADATA wsaDATA;
// initialize winsock
iResult = WSAStartup(wVersion, &wsaDATA);
if (0 != iResult) {
printf("WSAStartup failed: %d\n", iResult);
return -1;
}
Winsock清理: WSACleanup()
Winsock在程序推出之前需要使用WSACleanup()函数完成收尾工作,通常位于main函数的return之前。
获得本地主机名:gethostname()
可以获得本地的主机名
char hostname[NI_MAXHOST];
int hostlen = NI_MAXHOST;
// 获得本地主机名
gethostname(hostname, hostlen);
获得地址信息:getaddrinfo()
给定主机名和端口号,可以解析得到目标主机的网络地址(例如IP地址),类似ARP协议的作用。需要使用hints传入参数。
其中第一个参数为NULL时为获得本地主机信息。
当目标主机存在多个网卡/IP地址时,得到的result为一个链表,使用result->ai_next
连接。
char hostname[NI_MAXHOST] = “localhost”;
char servname[NI_MAXSERV] = “8080”;
struct addrinfo *result = NULL;
struct addrinfo hints;
// 初始化
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// 解析地址
iResult = getaddrinfo(hostname, servname, &hints, &result);
if (0 != iResult) {
printf("getaddrinfo failed: %d\n", iResult);
WSACleanup();
return -1;
}
获得主机名信息: getnameinfo()
给定目标主机的网络地址,可以解析得到主机名和端口号,类似RARP协议的作用。
char hostname[NI_MAXHOST];
char servname[NI_MAXSERV];
struct sockaddr_in sa;
int addrlen = sizeof(struct sockaddr);
u_short port = 8080;
const char* localhost = "127.0.0.1";
// 初始化
inet_pton(AF_INET, localhost, &sa.sin_addr.s_addr);
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
// 获得主机名信息
iResult = getnameinfo(
(struct sockaddr*) &sa, addrlen,
hostname, NI_MAXHOST,
servname, NI_MAXSERV,
0);
if (0 != iResult) {
printf("getnameinfo failed: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
清空地址信息: freeaddrinfo()
使用getaddrinfo得到的地址消息不再使用时,建议显式清空。
struct addrinfo *result; // 非空的地址信息指针
freeaddrinfo(result);
地址信息可视化
想要以可读方式显示地址信息,涉及到大端/小端的转换、二进制和点分十进制的转换等处理。
struct addrinfo *result; // 非空的地址信息指针
struct sockaddr_in server_addr;
char ipstringbuffer[46];
short port;
// 转换地址格式
memcpy(&server_addr, result->ai_addr, sizeof(server_addr));
inet_ntop(AF_INET, &server_addr.sin_addr, ipstringbuffer, sizeof(ipstringbuffer));
port = ntohs(server_addr.sin_port);
// 显示地址信息
printf("Server Address: %s\n", ipstringbuffer);
printf("Server Port Number: %d\n", port);
创建套接字: socket()
socket编程中核心的部分为套接字socket,创建socket的进程才可以实现不同主机上的进程间通信。
SOCKET s = INVALID_SOCKET;
// 创建套接字
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ConnectSocket == INVALID_SOCKET) {
printf("socket failed : %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
关闭套接字: closesocket()
套接字使用完毕后,建议手动关闭套接字。
SOCKET s; // 合法套接字
// 关闭套接字
closesocket(s);
绑定套接字: bind()
在服务器端创建的套接字需要绑定,即建立socket和地址之间的联系。相当于内部地址(进程描述符、套接字描述符)和外部地址(网络地址、端口号)之间建立连接。
SOCKET s; // 合法的套接字
struct addrinfo local;
// 绑定套接字
iResult = bind(s, local->ai_addr, (int)local->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed: %d\n", WSAGetLastError());
freeaddrinfo(local);
closesocket(s);
WSACleanup();
return 1;
}
监听端口: listen()
在服务器端绑定后的套接字可以监听端口,用于连接型的数据传输。类似TCP连接中等待SYN报文。
SOCKET s; // 合法的套接字
// 监听端口(套接字)
iResult = listen(s, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("listen failed: %d\n", WSAGetLastError());
closesocket(s);
WSACleanup();
return 1;
}
接受连接请求: accpet()
当服务器端接收到连接请求时(请求队列不为空),接受连接请求并创建连接。类似TCP连接中发送SYN ACK报文。此时创建了用于传输的套接字,与监听套接字分离。
SOCKET ListenSocket; // 合法的套接字
SOCKET ComSocket = INVALID_SOCKET;
struct sockaddr_in client_addr;
int addr_len = sizeof(struct sockaddr_in);
// 接受连接请求
ComSocket = accept(ListenSocket, (struct sockaddr*)&client_addr, &addr_len);
if (ComSocket == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
请求连接: connect()
客户端需要向服务器发送链接请求时,使用connect函数。类似TCP连接中发送SYN报文。
SOCKET s; // 合法套接字
struct addrinfo remote;
// 发送连接请求
iResult = connect(s, remote->ai_addr, (int)remote->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("connect failed: %d\n", WSAGetLastError());
closesocket(s);
s = INVALID_SOCKET;
}
关闭连接: shutdown()
通信结束时,不可以直接关闭socket,应该首先关闭连接(关闭发送方向的连接,表示不再发送消息)。类似TCP连接中的FIN报文。
SOCKET s;
// 关闭发送方向请求
iResult = shutdown(s, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed: %d\n", WSAGetLastError());
closesocket(s);
WSACleanup();
return -1;
}
接受数据: recv()
用于建立连接之后的数据传输。
#define RECV_BUFLEN 512
char recvbuf[RECV_BUFLEN];
int recvbuflen = RECV_BUFLEN;
SOCKET s; // 建立连接的套接字
memset(recvbuf, 0, recvbuflen);
// 接受数据
iResult = recv(s, recvbuf, recvbuflen, 0);
if (iResult > 0)
printf("Bytes received: %d\n", iResult);
else if (iResult == 0)
printf("Connection closed\n");
else
printf("recv failed: %d\n", WSAGetLastError());
发送数据: send()
用于建立连接之后的数据传输
#define SEND_BUFLEN 512
char sendbuf[SEND_BUFLEN];
snprintf(sendbuf, SEND_BUFLEN, "hello");
// 发送数据
iResult = send(ConnectSocket, sendbuf, int(strlen(sendbuf) + 1), 0);
if (iResult > 0)
printf("Bytes Sent: %d\n", iResult);
else
printf("send failed: %d\n", WSAGetLastError());
接受数据: recvfrom()
用于无连接的数据传输,由于是无连接的,需要在函数中传递地址指针保存数据发送方的网络地址信息。
#define DEFAULT_BUFLEN 512
SOCKET udp_s; // 合法的socket
char msgbuf[DEFAULT_BUFLEN];
struct sockaddr_in other_addr;
int addrlen = sizeof(other_addr);
char ipstringbuffer[46];
short port;
// 接受数据
iResult = recvfrom(udp_s, msgbuf, DEFAULT_BUFLEN, 0, (struct sockaddr*) &other_addr, &addrlen);
if (iResult == SOCKET_ERROR) {
printf("recvfrom() fail: %d\n", WSAGetLastError());
exit(EXIT_FAILURE);
}
inet_ntop(AF_INET, &si_other.sin_addr, ipstringbuffer, 46);
port = ntohs(si_other.sin_port);
printf("Receive %d bytes from %s:%d.\n", iResult, ipstringbuffer, port);
发送数据: sendto()
用于无连接的数据传输。其中的sockaddr数据是合法的地址数据。
#define DEFAULT_BUFLEN 512
SOCKET udp_s; // 合法的socket
char msgbuf[DEFAULT_BUFLEN];
struct sockaddr_in other_addr;
int addrlen = sizeof(other_addr);
snprintf(msgbuf, DEFAULT_BUFLEN, "hello");
// 发送数据
iResult = sendto(udp_s, msgbuf, int(strlen(msgbuf) + 1), 0, (struct sockaddr*) &other_addr, addrlen)
if (iResult == SOCKET_ERROR) {
printf("sendto() fail with error code: %d\n", WSAGetLastError());
exit(EXIT_FAILURE);
}
大/小端相关处理
winsock中数据结构sockaddr和addrinfo结构体中都是网络序保存。
大小端转换常用的函数包括ntohs、htons、ntohl、htonl。
函数名中的 h 表示 HOST 主机 序,n 表示 NETWORK 网络序,s 表示 short(16bit),l 表示 long(32bit)。
常用的函数inet_ntop 和 inet_pton 可以实现 IP 地址的
点分十进制和二进制格式之间的转换。通过第一个参数
AF_INET 或 AF_INET6 可以兼容 ipv4 和 ipv6。
struct sockaddr_in server_addr;
char ipstringbuffer[46];
short port;
// IP地址
inet_ntop(AF_INET, &server_addr.sin_addr, ipstringbuffer, sizeof(ipstringbuffer));
// 端口号
port = ntohs(server_addr.sin_port);
常见socket实体的实现
具体的函数模块的使用请参考上述代码。
TCP C/S结构
客户端:
int client(){
WSAStartup();
getaddrinfo(remote);
for addrinfo in linkList:
socket();
connect();
while(1){
send() or recv();
}
shutdown();
do{
recvlen = recv();
}while(recvlen>0);
closesocket();
WSACleanup();
}
服务器:
int server(){
WSAStartup();
getaddrinfo(local);
listenSocket = socket();
bind();
while(1){
listen(listenSocket);
tranSocket = accept();
createThread(tranSocket);
}
closesocket(listenSocket);
WSACleanup();
}
int serverThread(){
send() or recv();
shutdown();
do{
recvlen = recv();
}while(recvlen>0);
closesocket(tranSocket);
}
UDP 通信
int udpEntity(){
WSAStartup();
getaddrinfo(remote);
getaddrinfo(local);
socket();
bind();
createThread();
while(1){
sendto();
}
terminateThread();
closesocket();
WSACleanup();
}
int udpRecvThread(){
while(1){
recvfrom();
}
endthread();
}
ping命令(ICMP协议)
int ping(){
WSAStartup();
getaddrinfo(remote);
getaddrinfo(local);
socket();
bind();
while(1){
buildICMP();
sendto();
recvfrom();
}
WSACleanup();
}