一、前言:
Windows Sockets 规范为 Microsoft Windows 定义了一个二进制兼容网络编程接口。Windows Sockets 基于 Berkeley Software Distribution(BSD,4.3 版)中的 UNIX 套接字实现,后者是美国加州大学伯克利分校开发的。该规范包括针对 Windows 的 BSD 样式套接字例程和扩展。通过使用 Windows Sockets,应用程序能够在任何符合 Windows Sockets API 的网络上通信。
在 Win32 上,Windows Sockets 提供线程安全。许多网络软件供应商支持网络协议下的 Windows Sockets,这些协议包括:传输控制协议/网际协议 (TCP/IP)、Xerox 网络系统 (XNS)、Digital Equipment Corporation 的 DECNet 协议和 Novell Corporation 的互联网包交换协议/顺序分组报文交换协议 (IPX/SPX) 等。虽然目前的 Windows Sockets 规范定义了 TCP/IP 的套接字抽象化,但任何网络协议都可以通过提供自己版本的实现 Windows Sockets 的动态链接库 (DLL) 来满足 Windows Sockets。用 Windows Sockets 编写的商用应用程序示例包括 X Windows 服务器、终端模拟器和电子邮件系统。
注意: Windows Sockets 的用途是将基础网络抽象出来,这样,您不必对网络非常了解,并且您的应用程序可在任何支持套接字的网络上运行。因此,本文档不讨论网络协议的细节内容。
应用程序调用Windows Sockets的API实现相互之间的通讯。Windows Sockets又利用下层的网络通讯协议功能和操作系统调用实现实际的通讯工作。它们之间的关系如下图
二、SOCKET 基本概念
套接口可以根据通讯性质分类;这种性质对于用户是可见的。用户目前可以使用三种套接口,即流套接字、数据报套接字和原始套接字。
首先我们看看在Windows系统中三种类型套接字是怎样定义的:在WINSOCK.H中:
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3
三、字节序问题:
little endian和big endian是表示计算机字节顺序的两种格式,所谓的字节顺序指的是长度跨越多个字节的数据的存放形式.
假设从地址0x00000000开始的一个字中保存有数据0x1234abcd,那么在两种不同的内存顺序的机器上从字节的角度去看的话分别表示为:
1)little endian:在内存中的存放顺序是0x00000000-0xcd,0x00000001-0xab,0x00000002-0x34,0x00000003-0x12
2)big endian:在内存中的存放顺序是0x00000000-0x12,0x00000001-0x34,0x00000002-0xab,0x00000003-0xcd
需要特别说明的是,以上假设机器是每个内存单元以8位即一个字节为单位的.
简单的说,ittle endian把低字节存放在内存的低位;而big endian将低字节存放在内存的高位.
现在主流的CPU,intel系列的是采用的little endian的格式存放数据,而motorola系列的CPU采用的是big endian.
四、错误处理:
(1)INVALID_SOCKET:
在UNIX中所有句柄包括套接字句柄,都是非负的短整数。Windows Sockets句柄则没有这一限制,除了INVALID_SOCKET 不是一个有效的套接字外,套接字可以取从0到INVALID_SOCKET-1 之间的任意值。
所以检查在socket()和accept()函数返回值时,检查是否有错误发生就不应该使用把返回值和-1比较的方法,或判断返回值是否为负。取而代之的是,一个应用程序应该使用常量INVALID_SOCKET。
在WINSOCK.H中是这样定义的:
#define INVALID_SOCKET (SOCKET)(~0)
(2)常量SOCKET_ERROR是被用来检查Windows API调用失败的。虽然对这一常量的使用并不是强制性的,错误代码可以使用WSAGetLastError()调用得到。
在WINSOCK.H中是这样定义的:
#define SOCKET_ERROR (-1)
五、头文件(版本序号):
Winsock有两个主要版本,即Winsock 1和Winsock 2,二者都能在除Windows CE之外的所有Windows平台上运行。 Win Socket开发所必须需要的文件(以WinSock V2.0为例):
头文件:Winsock2.h
库文件:WS2_32.LIB
动态库:W32_32.DLL
六、Winsock基本的API:
(1)WSAStartup();本函数初始化一个WSADATA结构。
函数原型:int PASCAL WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
Windows Socket由DLL形式提供,为了完成一系列初始化操作,第一个使用Windows Socket的应用程序都必须进行WSAStarup()函数调用,并只有在成功地完成调用之后才能使用。现对此函数说明如下:
参数:
wVersionRequested
Windows Sockets API提供的调用方可使用的最高版本号.高位字节指出副版本(修正)号,低位字节指明主版本号.
lpWSAData
出参,指向WSADATA数据结构的指针,用来接收Windows Sockets执行的数据.
返回值:0表示成功。要得到错误码,请调用WSAGetLastError。下面是可能出现的错误值列表
WSASYSNOTREADY 基础网络子系统 没有准备好网络通信
WSAVERNOTSUPPORTED Windows Sockets版本不支持
WSAEINPROGRESS 一块Windows Sockets 1.1操作在进程中
WSAEPROCLIM Windows Sockets支持的任务数到达上线
WSAEFAULT lpWSAData不是一个有效指针
(2)socket():创建一个套接口。
所有的通信在建立之前都要创建一个Socket,该函数的功能与文件操作中的fopen类似。
函数原型:SOCKET PASCAL FAR socket( int af, int type, int protocol);
af:一个地址描述。目前仅支持AF_INET格式,也就是说ARPA Internet地址格式。
type:新套接口的类型描述。
protocol:套接口所用的协议。如调用者不想指定,可用0。
返回值:
若无错误发生,socket()返回引用新套接口的描述字。否则的话,返回INVAID_SOCKET错误,应用程序可通过WSAGetLastError()获取相应错误代码。
错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAEAFNOSUPPORT:不支持指定的地址族。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAEMFILE:无可用文件描述字。
WSAENOBUFS:无可用缓冲区,无法创建套接口。
WSAEPROTONOSUPPORT:不支持指定的协议。
WSAEPROTOTYPE:指定的协议不适用于本套接口。
WSAESOCKTNOSUPPORT:本地址族中不支持该类型套接口。
(3)bind():创建的Socket指定通信对象,将一本地地址与一套接口捆绑。
说明:成功创建了Socket之后,就应该选定通信的对象。首先是自己的程序要与网上的哪台计算机通话;其次,在多任务的系统下,该台计算机上可能会有几个程序在工作,必须指出要与哪个程序通信。前者可以通过Internet的网络IP地址来确定,而后都是由端口号来确定。用端口号来表示同一台计算机上不同的应用程序,端口号可以为0---65535,不同功能的通信程序使用不同的端口号,这样一台计算机上可以有几个程序同时使用一个IP地址通信而不互相干扰,IP地址与端口号的关系好像电话总机号码与分机号码的关系一样。在Internet上,1024以下的端口号已经被一些常用的网络服务如FTP(21),TELNET(23)等占用。所以,编制自己的通信程序时,应指定大于1024的端口号。要注意的是TCP和UDP的端口号是相互独立的,可以使用相同的端口号而不会相互干扰。
函数原型:int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR* name,
s:标识一未捆绑套接口的描述字。
name:赋予套接口的地址。sockaddr结构定义如下:
struct sockaddr{
u_short sa_family;
char sa_data[14];
};
namelen:name名字的长度。
SOCKADDR_IN
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族。
sa_data是14字节协议地址。
此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。但一般编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构
sockaddr_in(在netinet/in.h中定义):
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
typedef struct in_addr {
union {
struct{
unsigned char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct {
unsigned short s_w1,s_w2;
} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR;
sin_family指代协议族,在socket编程中只能是AF_INET
sin_port存储端口号(使用网络字节顺序)
sin_addr存储IP地址,使用in_addr这个数据结构
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
s_addr按照网络字节顺序存储IP地址
sockaddr_in和sockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向
sockadd的结构体,并代替它。也就是说,你可以使用sockaddr_in建立你所需要的信息,
然后用进行类型转换就可以了bzero((char*)&mysock,sizeof(mysock));//初始化
sockaddr_in mysock;
bzero((char*)&mysock,sizeof(mysock));
mysock.sa_family=AF_INET;
mysock.sin_port=htons(1234);//1234是端口号
mysock.sin_addr.s_addr=inet_addr("192.168.0.1");
(4)listen():设置等待连接状态。
函数原型:int listen(SOCKET s,int backlog);
对于服务器的程序,当申请到Socket,并指定通信对象为INADDR_ANY之后,就应该等待一个客户机的程序来要求连接。listen()就是把一个Socket设置这种状态的函数。
参数backlog是等待连接的队列长度,可取1~~5。如果当某个客户程序要求连接之时,服务器己与其他客户程序连接,则后来的连接请求会被放在队列中,等待服务器空闲的时候再与之连接。当队列达到指定长度(backlog的值)时,再来的连接请求都将被拒绝。
(5)accept():接受连接请求。
函数原型:SOCKET accept(SOCKET s,struct scokaddr_in *addr,int * addrlen);
当没有连接请求时,对于阻塞方式,就进入等待状态,直至有一个请求到达为止。accept()在接收到连接请求之后,会为这个连接建立一个新的SOCKET来与对方通信,并把它作为返回值。新建的SOCKET与原来的SOCKET有相同的特性,包括端口号。原来的SOCKET被释放,用于继续等待其他的连接请求,而新生成的SOCKET才是与客户端进行通信的实际SOCKET。所以一般将参数中的SOCKET称做“监听”SOCKET,它只负责接受连接,而不负责通信。参数中的指针addr和addrlen用来返回客户机的sockaddr_in结构体,通过addr可得到客户机的IP地址和连接端口。具体内容见bind()函数。
(注意:bind()、listen()、accept()函数一般都用于服务程序,属于被动等待的函数)
对客户程序,要主动提出连接请求,应使用connect()函数。
connect():建立与一个端的连接。
int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR* name,int namelen);
s:标识一个未连接套接口的描述字。
name:欲进行连接的端口名。
namelen:名字长度。
(6)send()/recv():发送、接收数据。
函数原型:
Int send(SOCKET s,char* buf,int len,int flags)
Int recv(SOCKET s,char* buf,int len,int flags)
S是连接用的。socket、buf和len是发送或接收的数据包及其长度,参数flags一般取0。recv ()函数实际上是读取send()函数发过来的一个数据包。当读到数据字节少于规定接收的数目时,就把数据全部接收,并返回实际接收到的字节数;当读到的数据多于规定的值是,在流方式下剩余的数据由下个recv()读出,在数据报文方式下多余的数据将被丢弃。这两个函数在出错时都返回SOCKET_ERROR。
以数据报文方式通信的SOCKET,由于事先不用建立连接,所以可以跳过connect()而直接用recvfrom和sendto两个函数通信:
Int recvfrom(SOCKET s,char* buf,int len,int flags,struct sockaddr_in from,int *fromlen);
Int sendto(SCOKET s,char * buf,int len,int flags,struct sockaddr_in to,int *tolen);
其中from、fromlen、to、tolen的含义和用法与bind()和connect()中的相同,分别表示接收和发送数据的对象。
(7)closesocket():关闭socket.
函数原型:closesocket(SOCKET s);
通信结束,关闭指定的SOCKET。
七、面向连接的流的方式调用过程
八:面向无连接的数据报过程: