Windows下的Socket编程——基本步骤

注意:本文所述内容,除非特别说明,均是在Windows环境下的配置。Linux等其他环境下,流程或者函数等或许会有不同。

基本流程:

  • 客户端:

    1. 初始化
    2. 调用socket分配套接字
    3. 设置套接口模式
    4. 填充SOCKADDR_IN来说明服务器地址
    5. 调用connect连接服务器
    6. 和服务器通信(调用send发送数据,或调用recv接收数据)。
    7. 调用closesocket关闭套接字
    8. 结束服务
  • 服务器端:

    1. 初始化
    2. 调用socket分配套接字
    3. 设置套接口模式
    4. 填充SOCKADDR_IN来说明服务器地址
    5. 调用bind绑定端口
    6. 调用listen监听端口
    7. 循环监听,监听中调用accept接受连接,调用sendrecv进行通信,调用closesocket关闭套接字。

这里我先只说明客户端和服务端共用的一些步骤或者函数。两者不一样的我在之后分开说明。


初始化:

初始化Winsock服务。

WSADATA wsa_data;
if (WSAStartup(MAKEWORD(1,1), &wsa_data)) {
    return -1;
}

WSADATA是一种数据结构,用来存储WSAStartup函数调用后返回的windows socket数据。其中结构很复杂,涉及到很多信息,这里主要涉及到的是其中的Word wVersion成员参数。这个参数是说明Windows Sockets规范版本的。其中高位字节存储副版本号,低位字节存储主版本号。

MAKEWORD是用来把两个数合并成一个WORD。

WSAStartup函数是异步套接字启动命令,也就是初始化winsock服务。它的第一个参数就是用来设置上面说的版本信息的,即程序请求的版本信息。具体操作系统返回的信息则由第二个参数,也就是上面说的WSADATA结构返回。所以这里申请使用的是1.1版本。该函数返回0则代表成功,否则返回错误代码:

  • WSASYSNOTREADY: 指出网络通信依赖的网络子系统还没有准备好。
  • WSAVERNOTSUPPORTED: 所需的Windows Sockets API的版本未由特定的Windows Sockets实现提供。
  • WSAEINVAL: 应用程序指出的Windows Sockets版本不被该DLL支持。

这里也可以通过LOBYTE(wsa_data.wVersion)以及HIBYTE(wsa_data.wVersion)函数来检查系统返回的版本和申请的版本是否一致。


结束服务

和初始化对应的就是结束Winsock服务。由于接下来的错误处理会涉及到,因此我把这个步骤搬到这里来说。这个步骤很简单,就是直接调用WSACleanup函数。

WSACleanup();

创建套接字:

调用socket函数来创建、分配套接字。

SOCKET socket_srv = socket(AF_INET, SOCK_STREAM, 0);
if (socket_srv == INVALID_SOCKET) {
	WSACleanup();
	return -1;
}

socket函数第一个参数用来说明协议域,协议域决定了socket地址类型。可选项有:AF_INETAF_INET6AF_LOCALAF_ROUTE等等。第一个对应IPV4,一般常用该类型;第二个对应IPV6;第三个又称AF_LOCAL,是Unix域的socket;第四个……我没查到。此处也有使用PF前缀的,如PF_INET。其实,PF代表协议族(Protocol Family),AF代表地址族(Address Family)。在Windows版本下,这两个宏没有任何区别。在Unix/Linux系统中才有微小差别(BSD是AF,POSIX是PF)。

第二个参数是套接口类型,这里用的SOCK_STREAM是数据流,对应TCP。还可以设置为SOCK_DGRAM数据报类型,即UDP;以及SOCK_RAW,这种借口允许对较低层次协议(如IP、ICMP)直接访问。

第三个参数则是指定协议。常用的有:IPPROTO_TCPIPPROTO_UDPIPPROTO_SCTPIPPROTO_TIPC等。0则对应着根据套接口类型选择对应的默认协议

返回值,成功时为套接字描述符,失败则返回INVALID_SOCKET
INVALID_SOCKETSOCKET_ERROR二进制码是一样的,之所以分开是因为,SOCKET类型本身是unsigned的,不存在-1。所以有的资料说失败返回-1,其实准确的来说是错误的)


关闭套接字

对应着创建套接字,关闭套接字顾名思义就是关闭连接并释放相应的资源,对应到底层就是常说的四次挥手。

closesocket(socket_client);

使用方法很简单,就是调用closesocket函数即可输入参数就是想要关闭的套接字。


设置套接口模式

这里是指设置阻塞、非阻塞等。具体什么是阻塞模式,我在这篇文章里写了,不知道的可以看一下。

u_long mode = 1;
if (ioctlsocket(socket_srv, FIONBIO, &mode) == SOCKET_ERROR) {
	closesocket(socket_srv);
	WSACleanup();
	return -1;
}

这个步骤主要是通过ioctlsocket这个函数实现。这个函数接受三个参数。

第一个参数为套接字描述符。也就是你要设置的套接字。

第二个参数是你要设置的模式。可选项有:

  • FIONBIO允许或者禁止非阻塞模式。如果第三个参数为0,则禁止非阻塞模式,否则允许非阻塞模式。套接口创建时,默认是阻塞的。另外WSAAsynSelect函数会将套接口设置为非阻塞。如果调用过WSAAsynSelect函数,那么通过ioctlsocket将套接口重新设置为阻塞的操作将会失败。如果想将套接口重新设置为阻塞模式,必须首先用WSAAsynSelect调用(IEvent参数置为0)来禁止WSAAsynSelect, 或者通过设置lNetworkEvents参数为0来调用WSAEventSelect
  • FIONREAD确定套接口自动读入的数据量。第三个参数指向一个无符号长整型,其中存有ioctlsocket的返回值。当时用该参数时,如果socket是SOCK_STREAM类型,则返回recv函数接受的所有数据量,通常与套接口中排队的数据总量相同,但也有特殊情况,因为一次recv函数调用能够读取的实际总数被限制在了一次send或者sendto函数调用能够写的数据总数;如果是SOCK_DGRAM类型,则返回套接口上排队的第一个数据报大小。
  • SIOCATMARK确实是否所有的带外数据都已被读入。这个命令仅适用于SOCK_STREAM类型的套接口,且该套接口已被设置为可以在线接收带外数据(SO_OOBINLINE)。如无带外数据等待读入,则该操作返回TRUE。否则的话返回FALSE,下一个recvrecvfrom操作将检索“标记”前一些或所有数据。应用程序可用SIOCATMARK操作来确定是否有数据剩下。如果在“紧急”(带外)数据前有常规数据,则按序接收这些数据(请注意,recvrecvfrom操作不会在一次调用中混淆常规数据与带外数据)。第三个参数指向一个BOOL型数,ioctlsocket在其中存入返回值。

第三个参数就是你要设置的值了。

返回值,该函数成功则返回0,否则返回SOCKET_ERROR。错误时可通过WSAGetLastError函数查询错误码。

错误码:

  • WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup
  • WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
  • WSAEINVAL:第二个参数为非法命令,或者第三个参数所指参数不适用于该命令,或者该命令不适用于此种类型的套接口。
  • WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
  • WSAENOTSOCK:描述字不是一个套接口。

填充地址信息:

SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(SOCKADDR));
srvAddr.sin_family = AF_INET;
srvAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
srvAddr.sin_port = htons(6000);

这里首先说明:所有用到这个地址信息的函数,其实都是以SOCKADDR结构体为参数的,但是由于某些历史原因,SOCKADDR是以char数组存放信息的,不利于数据填充。因此我们使用SOCKADDR_IN(或者全小写,两者是一样的)结构体来填充,然后强制类型转化为SOCKADR类型。

memset是用来将所有为置空,以防乱码。

SOCKADDR_IN结构体包含四部分:

  • sin_family:协议域,和之前一样。
  • sin_port:端口号。使用网络字节顺序,因此需要使用htons函数来将数字转化为网络字节顺序。
  • sin_addr:IP地址。如果想使用默认本机地址则将其赋值为htonl(INADDR_ANY)
  • sin_zero:是为了保持和SOCKADDR结构体大小相同而保留的空字节。

其中要说明的是sin_addr采用的是in_addr结构体类型。这个结构体定义如下:

typedef struct in_addr {
  union {
    struct { u_char  s_b1, s_b2, s_b3, s_b4; } S_un_b;
    struct { u_short s_w1, s_w2; } S_un_w;
    u_long S_addr;
  } S_un;
} IN_ADDR, *PIN_ADDR, *LPIN_ADDR;
#define s_addr  S_un.S_addr

inet_addr函数则是将字符串形式的ip地址转化为所需要的长整型(按网络字节顺序)。非法地址则返回INADDR_NONE

不过我在编译的时候提示:inet_addr已被弃用,推荐使用inet_pton,然而使用这个函数又发现没有定义。这是因为这个函数在另一个头文件WS2tcpip.h中(Linux下应该是arpa/inet.h,但是我没测试过,毕竟本文是在Windows下嘛,摊手┓( ´∀` )┏)。这个函数主要是同时支持IPV4和IPV6。这个函数使用方法如下:

inet_pton(AF_INET, SERVER_ADDR, &srv_addr.sin_addr);

发送信息

这里以客户端向服务器发送信息为例

if (send(sock_client, input, strlen(input) + 1, 0) == SOCKET_ERROR) {
	WSACleanup();
	return -1;
}

这里,input为要传输的char*类型的字符串。

另外,需要说明的是,send函数虽然叫发送,但其实他并不是发送send函数只是将要发送的数据拷贝到发送缓冲区,真正发送则由下层协议自动完成。所以即使拷贝成功,也有可能发送失败。这时下一个socket函数就会返回SOCKET_ERROR

send函数的参数都很简单易懂。
第一个参数,套接口,不用多说。
第二个参数,要发送的数据,char*类型,也没啥说的。
第三个参数,要发送的数据长度。这里+1主要是为了最后的'\0',不是为了暴力(雾)。
第四个参数……一般为0,别问是干什么的(问就是不知道)。(其实主要是我现在用不到……都是一些比较高级的设置,以后有空我再补充好了……)。

返回值,成功拷贝则返回实际拷贝的字节数,否则返回SOCKET_ERROR


接收信息

同样以客户端为例:

char recv_buff[1000];
int data_len;
while(1){
    data_len = recv(sock_client, recv_buff, 1000, 0);
    if (data_len == SOCKET_ERROR) {
        int err = WSAGetLastError();
        if(err == EAGAIN || err == EWOULDBLOCK || err = EINTR) continue;
        WSACleanup();
        return -1;
    }
    else if (data_len == 0) {
        WSACleanup();
        return -1;
    }
    break;
}

这里主要是使用recv函数,该函数需要注意的是:协议接收到的数据是有可能大于缓存大小的,这个时候需要多次调用recv函数来将接收到的数据全部拷贝完。我这里只做了简化版,认为一次就能接收完。实际上应该在发送的最开始约定一下发送的数据长度,或者在最末尾使用约定好的结束符。

recv函数同样接受4个参数。
第一个参数,套接口。
第二个参数,接收缓存,char*类型,用来存放recv函数接收到的数据。
第三个参数,缓存大小。
第四个参数,同样一般为0。

返回值,成功则返回拷贝的字节数;对面将连接关闭则返回0;失败则返回SOCKET_ERROR。失败时,错误码通过WSAGetLastError函数获取。

这里来说明一下错误码,这也是为什么这个接收写了这么长。错误码有太多太多了,我这里只挑几个常见的说:

  • WSAEWOULDBLOCK:这个错误常见于非阻塞模式下。意思是本次尝试并没有读到任何数据,或者尚未读完,建议重试或者改为阻塞模式。阻塞模式设置了等待时长时也会遇见这个错误。
  • WSAEINTR:这个错误是说被系统调用打断了操作,所以也应该重新尝试。
  • WSAEBADF:错误的socket描述符。
  • WSAEFAULT:缓存参数指向了无法存取的内存空间。
原创文章 34 获赞 41 访问量 5949

猜你喜欢

转载自blog.csdn.net/qq_44844115/article/details/103184128