WinSock
Winsock
- 编译采用了WINSOCK.H的应用程序时,需要链接到WS2_32.LIB库
- 使用WINSOCK.H时,需要链接到WSOCK32.LIB
- 使用MSWSOCK.H时,需要链接到MSWCOCK.DLL
初始化、反初始化
1. 加载Winsock库:
示例:
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
原型:
int
WSAAPI
WSAStartup(
__in WORD wVersionRequested, //用于指定准备加载的Winsock库的版本,高位字节是次版本,低字节是主版本。通常使用MAKEWORD(x,y)来指定【x是高位字节,y是低位字节】
__out LPWSADATA lpWSAData //用与其加载的库版本有关的信息填充这个结构
);
//WSADATA
typedef struct WSAData {
WORD wVersion; //设置为将要使用的Winsock版本
WORD wHighVersion; //包含了现有的Winsock库的最高版本
#ifdef _WIN64
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
#else
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets; //可以同时打开的最大套接字数量,不固定,尽量不要用这个值
unsigned short iMaxUdpDg; //数据报的最大长度,不固定,尽量不要用这个值
char FAR * lpVendorInfo;
#endif
} WSADATA, FAR * LPWSADATA;
2. 释放:
使用后需要调用WSACleanup函数,来使Winsock释放所有由Winsock分配的资源,并取消这个应用程序挂起的Winsock调用。
原型:
int WSAAPI WSACleanup( void );
错误检查和处理
出现错误时(比如SOCKET_ERROR),可以调用WSAGetLastError函数来获得一段代码,这段代码专用来说明错误
原型:
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
WSAGetLastError(
void
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
- 如果想要手动设置WSAGetLastError获取的错误代码,可以调用WSASetLastError。
- 注意:WSAStartup函数的调用不能使用WSAGetLastError来确定导致故障的特定错误(因为Winsock没有加载),用WSAStartup的返回状态判断(0是成功)
使用IP协议创建基本的Winsock调用来建立通信
- Winsock是一种独立于协议的接口
- IP是一种无连接协议,它不能确保数据传输的成功
- TCP和UDP通过IP进行面向连接和无连接的数据通信
IPv4寻址
程序通过sockaddr_in结构来指定IP地址和服务端口信息
示例:
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = PF_INET; //#define PF_INET AF_INET,该字段设为AF_INET,以告知Winsock此时正在使用IP地址族。
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(1234); //端口号在选择的时候,需要特别注意,某些端口是“已知的”服务保留的
sockaddr_in结构:
struct sockaddr_in {
short sin_family; //协议族
u_short sin_port; //端口号
struct in_addr sin_addr; //ip
char sin_zero[8]; //填充项,以使SOCKADDR_IN结构和SOCKADDR结构的长度一样
};
inet_addr函数可以将一个点分的IP转换成一个32位的无符号长整数。
字节排序
网络字节(network-byte)顺序:从最有意义到最无意义的字节排序(big-endian)
将一个数从主机字节顺序转换成网络字节顺序API(具体用法用到的时候再补充吧):
htol
WSAHtol
htons
WSAHtons
创建套接字
SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
函数原型:
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
__checkReturn
SOCKET
WSAAPI
socket(
__in int af, //协议的地址族
__in int type, //协议的套接字类型。TCP/IP对应SOCK_STREAM;UDP/IP对应SOCK_DGRAM
__in int protocol //用于在给定地址族和套接字类型具有多重入口时,对具体的传送作限定。TCP--IPPROTO_TCP;UDP--IPPROTO_UDP
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
面向连接的通信
- 在IP中,面向连接的通信是通过TCP/IP协议完成的。
- TCP提供两个计算机间可靠无误的数据传输。
在Winsock中,建立通信的步骤:
服务器:
1. 用Socket或WSASocker将给定的协议的套接字绑定到它已知的名称上
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
函数原型:
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
bind(
__in SOCKET s, //等待客户机连接的套接字
__in_bcount(namelen) const struct sockaddr FAR * name, //缓冲区,根据使用的协议,将实际地址填充到一个地址缓冲区,并在调用bind的时候将其转换为一个struct sockaddr
__in int namelen //要传递的、由协议决定的地址结构的长度
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
- 一旦出错,bind会返回SOCKET_ERROR。
- 对于bind而言,最常见的错误是WSAEADDINUSE。
- 如果使用的是TCP/IP,WSAEADDINUSE表示的是另一个进程已经同本地的IP接口及端口号绑定到了一起,或者那个IP和端口号处于TIME_WAIT状态。
- 假如对一个已被绑定的套接字调用bind,返回的会是WSAEFAULT错误。
2. 将套接字置为监听模式
listen(servSock, 20);
函数原型:
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
listen(
__in SOCKET s, //被绑定的套接字
__in int backlog //指定了被搁置的连接的最大队列长度。
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
- 因为在同一时间,可能会有多个服务器的连接请求,如果请求的数量过backlog,那么前面的backlog个请求被放在一个“挂起”队列中,以便应用程序(服务器)依次为他们提供服务。而backlog之后的连接请求都会失败,错误为WSAECONNREFUSED。
- 一旦服务器接收了一个连接,那个连接请求就会从队列中删去,以便别人可以继续发出请求。
- WSAEINVAL通常意味着,在调用listen之前没有调用bind。
3. 若一台客户机试图创立连接,服务器必须通过accept或WSAAccept调用来接受连接
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
函数原型:
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
__checkReturn
SOCKET //返回值:一个新的套接字,用于已经被接受的那个客户机的连接。(后续的消息发送什么的应该用这个套接字)
WSAAPI
accept(
__in SOCKET s, //监听的套接字
__out_bcount_opt(*addrlen) struct sockaddr FAR * addr, //新连接的地址信息(输出)
__inout_opt int FAR * addrlen //连接结构体的大小
);
#endif /* INCL_WINSOCK_API_PROTOTYPES */
- 对于连接成功的客户机,在后续的操作中应该使用accept返回的新的套接字
- 原本的监听套接字仍然用于接受其他客户机的连接,并且仍处于监听模式
- WSAAccept可以根据指定条件接受一个连接
客户机:
客户机的创建步骤:
- 创建一个套接字
- 建立一个SOCKADDR地址结构,结构名称为准备连接到的服务器名。(对于TCP/IP,这是客户机应用程序锁监听的服务器的IP地址和端口号)
- 用connect或WSAConnect初始化客户机与服务器的连接
套接字状态
连接:
- 对于每个套接字来说,它的初始状态都是CLOSED。
- 若客户机初始化了一个连接,就会向服务器发送一个SYN包,同时将客户机套接字的状态置为SYN_SENT。
- 服务器收到SYN包后,会发出一个SYN-ACK包,客户机需要用一个ACK包对它做出响应。此时,客户机的套接字将处于ESTABLISHED状态。
- 如果服务器一直不发送SYN-ACK包,客户机就会超时,并返回CLOSED状态。
- 服务器的套接字同本地接口及端口绑定起来,并在它上面进行监听,那么套接字的状态便是LISTEN。
- 客户机试图与服务器连接时,服务器就会收到一个SYN包,并用一个SYN-ACK包作出回应。此时,服务器套接字的状态就编程SYN_RCVD。
- 最后,客户机发出一个ACK包,它将使服务器套接字的状态变成ESTABLISHED。
关闭连接:
一旦应用程序处于ESTABLISHED状态,就可以通过两种方法来关闭它。
主动关闭:
- 应用程序会发出一个FIN包(应用程序调用closesocket或shutdown),并且套接字编程FIN_WAIT_1。
- 通信的对方会用一个ACK包作为回应,套接字的状态随之编程FIN_WAIT_2