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();
}
Lei Yang
Lei Yang
PhD candidate

My research interests include visual speech recognition and semantics segmentation.