一、简介
Windwos Scokects从Berkeley Sockets扩展而来,以动态链接库的形式提供给我们使用。windows sockets1.1和Berkeley Sockets都基于TCP/IP协议,他们中的很多函数都是一致的。如果我们采用双方共有的这些函数编写网络程序,将会有很好的移植性。
二、套接字的类型
1、流式套接字(SOCK_STREAM)
提供面向连接的、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。流式套接字实际上是基于TCP协议实现的。
2、数据报式套接字(SOCK_DGRAM)
提供无连接服务。数据包以独立包形式发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。数据报式套接字实际上是基于UDP协议实现的。
3、原始套接字(SOCK_RAW)
实现于系统核心。可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及以上的数据。
三、基于TCP的Socket编程
服务器端程序流程如下:
1、创建套接字(socket)。
2、将套接字绑定到一个本地地址和端口上(bind)。
3、将套接字设置为监听模式,准备接收客户请求(listen)。
4、等待客户请求到来:当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)。
5、用返回的套接字和客户端进行通信(send/recv)。
6、返回,等待另一客户的请求。
7、关闭套接字。
客户端程序流程如下:
1、创建套接子(socket)。
2、向服务器发送连接请求(connect)。
3、和服务器端进行通信(send/recv)。
4、关闭套接字。
PS:在客户端并不需要调用bind函数。因为服务器需要接收客户端的请求,所以必须告诉本地主机它打算在哪个IP地址和哪个端口上等待客户请求,因此必须调用bind函数来实现这一功能。而对客户端来说,当它发起连接请求,服务器端接受该请求后,在服务器端就保存了该客户端的IP地址和端口的信息。
四、基于UDP的Scokect编程
接收数据的一端叫服务器端,发送数据的一端叫客户端。
服务器端程序流程如下:
1、创建套接字(socket)。
2、将套接字绑定到一个本地地址和端口上(bind)。
3、等待接收数据(recvfrom)。
4、关闭套接字
客户端程序流程如下:
1、创建套接字(socket)。
2、向服务器发送数据(sendto)。
3、关闭套接字。
PS:虽然基于UDP的socket编程无需建立连接,但是服务器端必须告诉主机它是在哪一个地址和端口上等待数据的到来。
五、相关函数简介
1、WSAStartup函数
函数原型:int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
利用套接字编程时,第一步需要加载套接字库,通过WSAStartup函数来实现。功能:a、加载套接字库;b、进行套接字库的版本协商,即确定将使用的socket版本。
函数参数:
<1>wVersionRequested:用来指定准备加载的Winsock库德版本。高位字节指定Winsock库的副版本,低位字节则是主版本。通常版本号为:2.1,2就是主版本号,1就是副版本号。可以利用MAKEWORD(x,y)宏方便地获得wVersionRequested的正确值,x为高位字节,y为低位字节。
<2>lpWSAData:指向WSADATA结构的指针,WSAStartup函数用其加载的库版本有关信息填在这个结构中。
WSADATA结构的定义如下:
typedef struct WSAData
{
//打算使用的Winsock版本
WORD wVersion;
//现有的Winsock库的最高版本
WORD wHighVersion;
//下面两个数组由特定的Winsock实施方案设定,事实上并没有用
char szDescription[WSADESCRIPTION_LEN + 1];
char szSystemStatus[WSASYS_STATUS_LEN + 1];
//同时最多可打开多少套接字,不是固定的,和可用物理内存有关
unsigned short iMaxSockets;
//数据报的最大长度,通过WSAEnumProtocols函数来查询协议信息获得
unsigned short iMaxUdpDg;
//为Winsock实施方案有关的指定厂商信息预留的,任何一个Win32平台都没使用这个字段
char FAR * lpVendorInfo;
}WSADATA,*LPWSADATA;
PS:Ws2_32.dll或底层网络子系统没有正确地初始化或没找到,WSAStartup函数返回WSASYSNOTREADY。wVersion是动态库所支持的最高版本与请求版本中较小的那个。如果请求的版本低于Winsock动态库所支持的最低版本,WSAStartup函数返回WSAVERNOTSUPPORTED。成功调用WSAStartup函数后,在最后对应一个WSACleanUp调用,以便释放为应用程序分配的资源,终止对Winsock动态库的使用。
2、socket函数
函数原型:SOCKET socket(int af, int type, int protocol);
加载了套接字库之后,就可以调用socket函数创建套接字了。
函数参数:
<1>af:指定地址簇,对于TCP/IP协议的套接字,它只能是AF_INET(也可写成PF_INET);
<2>type:指定socket类型,对于1.1版本的socket,它只支持两种类型的套接字,SOCK_STREAM和SOCK_DGRAM;
<3>protocol:与特定的地址家族相关的协议,如果指定为0,那么系统会根据地址格式和套接字类别,自动选择一个合适的协议(推荐使用此方法)。
PS:如果函数调用成功,返回一个新的SOCKET数据类型的套接字描述符;若调用失败,这个函数会返回一个INVALID_SOCKET值,错误信息可以通过WSAGetLastError函数返回。
3、bind函数
函数原型:int bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
创建套接字后,应该将该套接字绑定到本地的某个地址和端口上,这需要通过bind函数来实现。
函数参数:
<1>s:指定要绑定的套接字;
<2>name:指定了该套接字的本地地址信息,这是一个指向sockaddr结构的指针变量,由于该地址结构是为所有的地址家族准备的,这个结构可能随所使用的网络协议不同而不同,所以,要用第三个参数指定该地址的结构的长度。
<3>namelen:见name参数解析。
sockaddr结构的定义如下:
struct sockaddr
{
//指定地址家族,对于TCP/IP协议的套接字,必须设置为AF_INET
u_short sa_family;
//仅仅表示要求一块内存分配区,起到占位的作用,该区域中指定与协议相关的具体地址信息.由于实际要求的只是内存区,所以对不同的协议家族,用不同的结构来替换sockaddr。
char sa_data[14];
};
在基于TCP/IP的socket编辑过程中,可以用sockaddr_in结构替换sockaddr,以方便我们填写地址信息,sockaddr_in结构体的定义如下:
struct sockaddr_in
{
//表示地址簇,对于IP地址,sin_family将一直是AF_INET
short sin_family;
//指定的是将要分配给套接字的端口
unsigned short sin_port;
//套接字主机的IP地址
struct in_addr sin_addr;
//只是一个填充数,以使sockaddr_in结构和sockaddr结构的长度一样
char sin_zero[8];
};
PS:1、如果函数调用成功,返回0;如果调用失败,返回SOCKET_ERROR,错误信息可以通过WSAGetLastError函数返回。
2、sockaddr_in结构中sin_addr的结构是in_addr,该结构的定义如下:
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;
};
通常利用这个结构将一个点分十进制格式的IP地址转换为u_long类型,并将结构赋给成员S_addr。
4、inet_addr和inet_ntoa函数
可以将IP地址指定为INADDR_ANY,允许套接字向任何分配给本地机器的IP地址发送或接收数据。有的机器可能会有多个网卡,每个网卡都可以有自己的IP地址,用INADDR_ANY可以简化应用程序的编写。将地址指定为INADDR_ANY,将允许一个独立应用接受发自多个接口的回应。如果我们只想让套接字使用多个IP地址中的一个地址,就必须指定实际地址,可以用inet_addr函数来实现,该函数的原型声明如下:
unsigned long inet_addr { const char FAR * cp };
inet_addr需要一个字符串作为参数,该字符串指定了以点分十进制格式表示的IP地址。而且inet_addr函数会返回一个适合分配给S_addr的u_long类型的值。
inet_ntoa函数会完成相反的转换,它接受一个in_addr结构体类型的参数并返回一个以点分十进制格式表示的IP地址字符串。该函数原型如下:
char FAR * inet_ntoa( struct in_addr in );
5、listen函数
函数原型:
int listen( SOCKET s,int backlog );
listen函数的作用是将指定的套接字设置为监听模式。
函数参数:
<1>s:套接字描述符。
<2>backlog:是等待连续队列的最大长度。如果设置为SOMAXCONN,那么下层的服务提供者将负责将这个套接字设置为最大的合理值。如果将backlog设置为2,当有3个请求同时到来时,前两个连续请求就会被放到等待请求连接队列中,然后由应用程序依次为这些请求服务,而第3个连接请求就被拒绝了。
6、accept函数
函数原型:
SOCKET accept( SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen);
接受客户端发送的连接请求。
函数参数:
<1>s:套接字描述符,该套接字已经通过listen函数将其设置为监听状态;
<2>addr:指向一个缓冲区的指针,该缓冲区用来接收连接实体的地址,也就是客户端向服务器发起连接,服务器接受这个连接时,保存发起连接的这个客户端的IP地址信息和端口信息;
<3>addrlen:指向一个整形的指针,返回包含地址信息的长度。
7、send函数
函数原型:
int send( SOCKET s, const char FAR * buf, int len, int flags );
通过一个已经建立连接的套接字发送数据。
函数参数:
<1>s:一个已建立连接的套接字;
<2>buf:指向一个缓冲区,该缓冲区包含将要传递的数据;
<3>len:缓冲区的长度;
<4>flags:设定的值将影响函数的行为,一般将其设置为0。
8、recv函数
函数原型:
int recv( SOCKET s, char FAR * buf, int len, int flags );
从一个已连接的套接字接收数据。
函数参数:
<1>s:建立连接后,准备接收数据的那个套接字;
<2>buf:一个指向缓冲区的指针,用来保存接收的数据;
<3>len:缓冲区的长度;
<4>flags:和send的第四个参数类似。
9、connect函数
函数原型:
int connect( SOCKET s, const struct sockaddr FAR * name, int namelen);
将与一个特定的套接字建立连接。
函数参数:
<1>s:即将在其上建立连接的那个套接字;
<2>name:设定连接的服务器端地址信息;
<3>namelen:指定服务器端地址的长度。
10、recvfrom函数
函数原型:
int recvfrom(SOCKET s, char FAR * buf, int len, int flags, struct sockaddr FAR * from, int FAR * fromlen);
将接收一个数据报信息并保存源地址。
函数参数:
<1>s:准备接收数据的套接字;
<2>buf:指向一个缓冲区指针,该缓冲区用来接收数据;
<3>len:缓冲区的长度;
<4>flags:和send函数的第四个参数类似;
<5>from:指向地址结构体的指针,主要是用来接收发送方的地址信息;
<6>fromlen:是一个in/out类型的参数,表明在调用前需要给它指定一个初始值,当函数调用之后,会通过这个参数返回一个值,该返回值是地址结构的大小。
11、sendto函数
函数原型:
int sendto( SOCKET s, const char FAR * buf, int len, int flags, const struct sockaddr FAR * to,int tolen );
函数参数:
<1>s:一个套接字描述符(可能已经建立);
<2>buf:指向一个缓冲区指针,该缓冲区包含将要发送的数据;
<3>len:缓冲区的长度;
<4>flags:和send函数的第四个参数类似;
<5>to:是一个可选的指针,指定目标套接字的地址;
<6>tolen:是参数to中指定的地址的长度。
12、htons和htonl函数
htons函数将把一个u_short类型的值从主机字节顺序转换为TCP/IP网络字节顺序。原型如下:
u_short htons( u_short hostshort );
参数hostshort是一个以主机字节顺序表示的16位数值。
htonl函数将把一个u_long类型的值从主机字节顺序转换为TCP/IP网络字节顺序。原型如下:
u_long htonl(u_long hostlong);
参数hostlong是一个以主机字节顺序表示的32位数值。