1. Winsock是微软做的网络通讯库,以前winsock是1.1版本,现在winsock有了winsock 2.2版本,winsock2版本变动比较大,做了很多工作。Winsock的接口设计在很大程度上参考了UNIX平台上的BSD的socket实现,在 Winsock 2里面接口做了一些变动,目的是做到winsock真正和协议无关,是一个通用的开发平台。
2. 本章介绍了TCP和UPD的两个最简单的winsock例子,实际上,这样的例子对于一般的网络通讯程序来说已经够用了。这些例子都是block的网络通讯方式,第五章会讲解non-block的winsock编程。
3. Winsock 1和Winsock 2的函数的命名方式。一般来说,Winsock 2的函数都以WSA开头,比如创建一个socket在winsock 1中就是调用socket函数,在winsock 2中我们就可以使用WSASocket,相比socket,WSASocket提供了更多的特性。Winsock 2兼容Winsock 1的所有函数。上述的命名方式有一些例外,他们是:WSAStartup, WSACleanup, WSARecvEx, and WSAGetLastError,这四个函数在Winsock
1中就定义了。
4. winsock的头文件和lib文件。这是开发winsock程序必须的了。在目前大部分的windows平台下,winsock 2都是ready的。Windows CE只支持winsock 1。在开发winsock 2的程序的时候,我们需要include winsock2.h,在开发winsock 1的程序的时候,我们需要include winsock.h。还有一个叫做mswsock.h,这里面定义的函数是只有在微软平台上运行的函数,这些函数能提供高性能,在我们书写需要高性能的网 络通讯程序的时候,我们使用这些函数,具体内容在第六章中描述。
lib文件。winsock 2的程序需要链接ws2_32.lib, winsock 1的程序需要链接wsock32.lib,如果使用mswsock,需要链接mswsock.dll
5. Winsock初始化。调用WSAStartup可以初始化winsock,也就是程序load winsock的dll文件。如果没有初始化winsock就调用了winsock中的函数,函数会返回 SOCKET_ERROR,SOCKET_ERROR是一个generic的返回值表示winsock操作失败,详细的错误信息可以通过调用 WSAGetLastError来获得,对于上述描述的错误,得到的错误码是WSANOTINITIALISED。WSAStartup函数的原型如下:
-
Code: Select
all -
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
wVersionRequested 参数用来指定我们要使用的winsock版本。WORD中高字节部分用来指定使用的winsock的minor版本,低字节用来指定使用winsock的 major版本。我们可以使用MAKEWORD(x,y)宏来生成这样一个WORD, x是minor version,y是major version。
lpWSAData参数是一个struct指针,WSAStartup函数会填写这个struct中的内容,这个struct的定义是:
-
Code: Select
all -
typedef struct WSAData
{
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN + 1];
char szSystemStatus[WSASYS_STATUS_LEN + 1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
} WSADATA, * LPWSADATA;
wVersion -- 使用的winsock版本
wHighVersion -- 目前平台上可用的winsock的最高版本
szDescription, szSystemStatus -- 用作特殊用途,一般不常用
iMaxSockets, iMaxUdpDg -- 不用使用这两个字段。他们定义了最大的并发连接数和最大的udp datagram size。然而,要找出正确的这些值,可能还要参考协议本身中的一些限制。
lpVendorInfo -- 保留用,用来存放vendor-specific information.不用使用这个字段。
6. 各种windows平台支持的winsock版本:
附件1
可见,大部分windows都支持winsock 2
7. winsock 1的程序可以在支持winsock 2的windows上良好运行。实际上,在winsock 2的windows上,所有的winsock 1的请求都会被映射到winsock 2的dll中。微软的兼容工作做的还是不错的。如果我们在WSAStartup中申请了一个高于目前平台支持的winsock版本,WSAStartup 会失败,同时WSADATA中的wHighVersion中会存放该平台上支持的winsock的最高版本。
8. 调用WSACleanup用来结束一个winsock程序。
int WSACleanup(void);
这个函数会释放所有资源,关闭所有pending的请求等。实际上,就算我们的程序不调用这个函数,操作系统也会把这些资源回收,但是,一个良好的程序,必须调用这个函数。
9. 错误处理。前面说过了,大多数的winsock函数失败的时候,都会返回SOCKET_ERROR,使用WSAGetLastError能获得详细的错误 码,这些错误码都定义在winsock2.h, winsock.h中。有些函数例外,他们不返回SOCKET_ERROR而是其他的返回值。下面是一个使用WSAGetLastError函数的例子:
-
Code: Select
all -
#include <winsock2.h>
void main(void)
{
WSADATA wsaData;
// Initialize Winsock version 2.2
if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
{
// NOTE: Since Winsock failed to load we cannot use
// WSAGetLastError to determine the specific error for
// why it failed. Instead we can rely on the return
// status of WSAStartup.
printf("WSAStartup failed with error %d\n", Ret);
return;
}
// Setup Winsock communication code here
// When your application is finished call WSACleanup
if (WSACleanup() == SOCKET_ERROR)
{
printf("WSACleanup failed with error %d\n", WSAGetLastError());
}
}
10. Addressing a Protocol. 本节只介绍基于IP的协议,参考第三章有很多关于网络协议的东西。本节只介绍在winsock中定义一个IPv4的信息结构。定义这样的结构对于winsock中的bind等这样的函数来说是必须的。
-
Code: Select
all -
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sockaddr_in其实是针对Internet,也就是TCP/IP的一个结构。在后面我们会看到,任何结构将来都会转换成SOCKADDR这个结构,有点类似SOCKADDR是基类,sockaddr_in这些都是SOCKADDR的派生类一样。
sin_family -- 必须设成AF_INET,表示我们要使用IP地址family
sin_port -- 指定端口,不过要考虑网络次序和主机次序的问题,下面会介绍
sin_addr -- 指定IP地址。其实是指定这个struct中的一个字段。IP地址也是以long的类型存放的,也要处理网络次序和主机次序的问题。微软提供了一个function来让我们把一个IP字符串转换成一个long型的数值。这个函数是:
-
Code: Select
all -
unsigned long inet_addr(
const char FAR *cp
);
注意,调用了inet_addr函数之后生成的long,就不需要再做主机次序,网络次序的转换了。但是后面会看到,如果没用inet_addr,比如给出的IP地址是INADDR_ANY的话,还是要调用htonl函数的。
sin_zero -- 没有用,完全是放在这里为了和SOCKADDR的大小兼容
11. 主机次序和网络次序。这个问题的起源是因为在上述的结构中,我们需要指定一个数字类型的变量,比如port和long型的IP地址。由于数字是多字节的一 块内容,所以就有字节摆放顺序的问题。众所周知,数字类型的变量在不同的计算机(主机)中存放的次序是不一样的,有把高位字节存在前面,低位字节存在后面 的,也有反过来的。对于网络通讯来说,必需要把这种情况统一起来,否则这些关键的信息就会解析错误了。于是就有了所谓的主机次序和网络次序的相互转换了。
winsock中提供了一组函数用来做这个事情,下面的四个函数用来将主机次序转成网络次序:
-
Code: Select
all -
u_long htonl(u_long hostlong);
int WSAHtonl(
SOCKET s,
u_long hostlong,
u_long FAR * lpnetlong
);
u_short htons(u_short hostshort);
int WSAHtons(
SOCKET s,
u_short hostshort,
u_short FAR * lpnetshort
);
带 l的是处理4字节的数,带s的是处理2字节的数。不带WSA的函数传入主机次序的数值,返回网络次序的数;带WSA的是把主机次序的数传入 hostlong, hostshort参数,处理后的网络次序的数被填充在lpnetlong, lpnetshort指针指向的变量中。
以下四个函数从网络次序转成主机次序:
-
Code: Select
all -
u_long ntohl(u_long netlong);
int WSANtohl(
SOCKET s,
u_long netlong,
u_long FAR * lphostlong
);
u_short ntohs(u_short netshort);
int WSANtohs(
SOCKET s,
u_short netshort,
u_short FAR * lphostshort
);
12. 给出一个例子用来示范怎么创建这么一个sockadd_in的东西:
-
Code: Select
all -
SOCKADDR_IN InternetAddr;
INT nPortId = 5150;
InternetAddr.sin_family = AF_INET;
// Convert the proposed dotted Internet address 136.149.3.29
// to a four-byte integer, and assign it to sin_addr
InternetAddr.sin_addr.s_addr = inet_addr("136.149.3.29");
// The nPortId variable is stored in host-byte order. Convert
// nPortId to network-byte order, and assign it to sin_port.
InternetAddr.sin_port = htons(nPortId);
第三章会讲述如何在填写IP地址的时候,填写一个域名,以及winsock中包含的一些域名和IP地址互转换的函数。
13. 创建一个socket。socket是网络通讯程序中必须的一个数据结构,他不同于文件描述符,他有单独的数据类型定义-SOCKET。创建一个 socket可以调用socket方法或WSASocket。这里我们用socket函数来举例,后面会详细介绍这些方法。
-
Code: Select
all -
SOCKET socket (
int af,
int type,
int protocol
);
af -- 定义协议的address family,对于IPv4,当然是AF_INET
type -- 定义socket类型。对于TCP,填写SOCK_STREAM,对于UDP,填写SOCK_DGRAM
protocol -- 定义协议。如果我们在af或type中定义了多种类型的话,这里可以定义一个协议的组合。对于TCP,设置此项为IPPROTO_TCP,对于UDP,设置为IPPROTO_UDP
bind:
-
Code: Select
all -
int bind(
SOCKET s,
const struct sockaddr FAR* name,
int namelen
);
示例代码:
-
Code: Select
all -
SOCKET s;
SOCKADDR_IN tcpaddr;
int port = 5150;
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
tcpaddr.sin_family = AF_INET;
tcpaddr.sin_port = htons(port);
tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));
这里IP地址指定成INADDR_ANY,表示绑定在本机,也就是说,如果本机有多块网卡的话,往任何一块网卡上指定的端口上发送的数据都能被上述程序得到。
bind 的错误处理。bind失败,返回SOCKET_ERROR,错误码是WSAEADDRINUSE,表示本机有另外一个进程已经使用了我们指定的IP地址和 端口或者是这个IP地址或端口处于TIME_WAIT状态(TIME_WAIT状态在下面会描述);此外,如果我们bind了一次之后又再次往同样的端 口,IP bind,那错误码是WSAEFAULT。
listen:
-
Code: Select
all -
int listen(
SOCKET s,
int backlog
);
backlog -- 处理并发请求的数量。比如设成2,如果同时有三个并发请求过来,那么前两个会放入排队队列,第三个请求就会被拒绝,同时返回 WSAECONNREFUSED错误。一旦一个请求被accept处理之后,排队队列中就会清除这个请求,新的请求就可以进来了。backlog的最大值 和协议有关系,没有一个确切的方法可以给出一个具体的最大值,如果我们指定的backlog值超过了允许的范围,函数会帮我们把这个值设成最大允许的值。
listen的错误处理,一般返回WSAEINVAL,表示在listen之前没有调用bind。
accept:
-
Code: Select
all -
SOCKET accept(
SOCKET s,
struct sockaddr FAR* addr,
int FAR* addrlen
);
accept 中除了socket之外的两个参数是OUT类型的,也就是accept会填写这两个参数。accept会取出排队队列中的第一个request,然后处 理,addr中会存放client的IP地址,端口等信息,addrlen中存放的是addr结构的大小。此外,accept返回一个SOCKET变量, 利用这个socket变量,server端程序就能和client端程序进行send/recv这样的操作了。
accept的错误处理, 出错时,accept的返回值是INVALID_SOCKET,错误码有WSAEWOULDBLOCK,当我们使用非阻塞的non-block的 listen方法,而当前队列中没有可服务的request的时候,会产生这个错误码。WSAAccept方法是winsock 2中的方法,这个方法比accept增强了一些特性,比如可以给定一个condition,这个condition返回true的时候才accept一个 connection。具体看第10章
下面给出示例代码,这段代码是核心代码演示,不一定完整,而且这段代码只会accept一次就退出了,正常程序应该有个while循环:
-
Code: Select
all -
#include <winsock2.h>
void main(void)
{
WSADATA wsaData;
SOCKET ListeningSocket;
SOCKET NewConnection;