本章介绍如何编写一个 Winsock TCP/IP服务端来接收客户连接请求
通讯分为面向连接通讯(Connection-Oriented Communication 如TCP)和非连接通讯(Connectionless Communication 如UDP)。笔记(3)至笔记(6)将介绍前者

SOCKET 是Winsock中独立的一个类型,用来表示一个连接的句柄
它的定义如下:

  1. typedef UINT_PTR SOCKET;
  2. typedef _W64 unsigned int UINT_PTR, *PUINT_PTR;

在Winsock中,有两种基本通讯方式:面向连接传输模式、无连接传输模式
IP协议族中,TCP/IP协议做到了面向连接通讯,TCP提供零错误数据传输机制,在发送方和接收方建立一条虚拟的连接.
服务端是一个进程,它等待若干客户连接并相应它们的请求.
服务端在一个周知地址上监听连接,在TCP/IP中,周知端口是本地接口的IP地址和端口号.
不同的协议有不同的地址组成方式因此有不同的地址命名方法.

建立一个服务端的步骤

  1. socket()/WSASocket(); //创建一个socket
  2. bind(); //绑定一个周知地址
  3. listen(); //设置监听模式
  4. accept()/WSAAccept(); //接收连接

1.创建一个socket

socket()/WSASocket()
有两个创建Socket的方法:socket()、WSASocket().

  1. SOCKET socket(
  2. int af,
  3. int type,
  4. int protocol
  5. );

int af:一个地址描述。目前仅支持AF_INET格式,也就是说ARPA Internet地址格式
int type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
int protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
Winsock提供了四个很有用的函数来控制socket的选项和行为:setsockopt()、getsockopt()、ioctlsocket()、WSAIoctl().将在后面详细介绍

2.绑定一个周知地址

bind()

  1. int bind(
  2. SOCKET s, // 服务端socket
  3. const struct sockaddr FAR * name, // SOCKADDR
  4. int namelen // name长度
  5. );

使用代码如下

  1. SOCKET s;
  2. SOCKADDR_IN InternetAddr;
  3. s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // 创建socket
  4. INT nPortID=5150;
  5. InternetAddr.sin_family=AF_INET;
  6. InternetAddr.sin_addr.s_addr=htonl(INADDR_ANY); // INADDR_ANY参数表示该服务端接收任意地址往该端口发送的消息
  7. InternetAddr.sin_port=htons(nPortID);
  8. bind(s,(SOCKADDR*)&InternetAddr,sizeof(InternetAddr)); // 显式转换

错误时返回SOCKET_ERROR.
常见错误码:

  • WSAEADDRINUSE:该IP端口已被占用或者处于TIME_WAIT状态.
  • WSAEFAULT:已经绑定过后再一次绑定.

3.设置监听模式

listen()

  1. int listen(
  2. SOCKET s,
  3. int backlog //挂起连接队列最大长度
  4. );

设置backlog参数后仍然需要由程序底层来进行适当替换,替换为一个适合的接近值,并且用户无法得知确切的值.一般设置为5.
当挂起队列满时,连接请求会失败,返回WSAECONNREFUSED
常见错误码:

  • WSAEADDRINUSE:该IP端口已被占用或者处于TIME_WAIT状态.
  • WSAEFAULT:listen()之前会进行bind().

4.接收连接

accept()/WSAAccept()
其实还有AcceptEx()函数,是accept()的扩展,在后面会介绍

  1. SOCKET accept(
  2. socket s, // 监听状态绑定的socket
  3. struct sockaddr FAR * addr, // 有效的SOCKADDR_IN地址
  4. int FAR * addrlen // SOCKADDR_IN的长度
  5. );

该函数返回一个关联到请求连接的客户的socket描述符,然后使用这个socket来进行后续一系列操作.
服务端监听socket将持续存在来监听其他客户的请求.
发生错误时,返回INVALID_SOCKET错误.
常见错误码:

  • WSAEWOULDBLOCK:监听socket在异步或非阻塞模式并且无连接可以接受.

一个完整的 Winsock TCP/IP 服务端代码如下

  1. int main(void)
  2. {
  3. WSADATA wsaData;
  4. SOCKET ListeningSocket;
  5. SOCKET NewConnection;
  6. SOCKADDR_IN ServerAddr;
  7. SOCKADDR_IN ClientAddr;
  8. int port=5150;
  9. int ClientAddrLen;
  10. WSAStartup(MAKEWORD(2,2),&wsaData); // 初始化 Winsock 2.2 版本
  11. ListeningSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // 创建一个 socket 来监听客户连接
  12. ServerAddr.sin_family=AF_INET; // 填充 SOCKADDR_IN 数据结构
  13. ServerAddr.sin_port=htons(port);
  14. ServerAddr.sin_addr.s_addr=htonl(ADDR_ANY);
  15. bind(ListeningSocket,(SOCKADDR*)&ServerAddr,sizeof(ServerAddr)); // 绑定一个周知地址
  16. listen(ListeningSocket,5); // 设置监听模式
  17. ClientAddrLesizeof(ClientAddr); // 显示指定ClientAddrLen大小
  18. NewConnection=accept(ListeningSocket,(SOCKADDR*)&ClientAddr,&ClientAddrLen);
  19. /*
  20. * 接受一个到来的连接,注意!最后一个参数需要自己显式指定!
  21. * 这里你通过这些socket可以做两件事
  22. * 1.通过ListeningSocket再次调用accept()来接受其他连接
  23. * 2.通过NewConnection来发送/接受数据
  24. * 当你做完这两件事情时必须要关闭这些socket
  25. * socket的关闭将在后面介绍
  26. */
  27. closesocket(ListeningSocket); // 关闭ListeningSocket
  28. closesocket(NewConnection); // 关闭NewConnection
  29. WSACleanup(); // 关闭Winsock
  30. return 0;
  31. }

注意 accept()函数的最后一个参数需要显式指定大小!
ClientAddrLesizeof(ClientAddr); //显式指定ClientAddrLen大小
好了,到目前为止你已经可以编写一个 Winsock TCP/IP服务端来接收客户连接请求了.