Windows下的网络应用开发大部分是通过Winsock完成的(除了Winsock 以外还有其他的),Winsock有两种开发模式,一种是阻塞模式,另一种是非阻塞模式。阻塞模式是基本同步的开发模式,非阻塞模式是基于异步的开发模式。非阻塞模式结合了Windows的消息机制,更符合Windows下的开发。
Winsock的相关函数
每个需要使用Winsock进行网络开发的Widnows 应用程序都必须包含Winsock2.h(这是的二个版本的Winsock库),除了这个以外,还有一个静态库ws2_32.lib 。在使用它们的时候要对这个库进行一次初始化,使用完毕之后要释放该库,下面分别介绍这两个函数。
首先来看初始化 ws2_32.dll 动态链接库的函数:
int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData );
这个函数是用来初始化 ws2_32.dll动态链接库的,这个动态链接库是所有网络应用程序都会加载的动态链接库,在使用这个动态链接库时就需要用 WSAStartup() 函数进行初始化。如果不初始化这个动态链接库,其余相关的基于这个动态链接库的网络函数的调用都会失败。
参数说明如下:
(1)wVersionRequested : Windows Sockets API 提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指出主版本号。
(2)lpWSAData : 指向 WSADATA 数据结构的的指针,用来接收 Windows sockets 实现的细节。
释放 ws2_32.dll 动态链接库:
WSACleanup();//释放套接字库这个函数是结束这个动态链接库的,一般在程序退出时使用。
创建套接字:
SOCKET socket( int af, int type, int protocol );
参数说明如下:
(1) af :指定应用程序使用的通信协议族,对于TCP/IP 协议族,该参数始终为 PF_INET 。也有一些使用的是 AF_INET。AF_INET 是地址族,虽然使用这个没错,但还是建议使用 PF_INET.
(2) type :指定要创建的套接字的类型,流套接字类型为 SOCK_STREAM, 数据包套接字类型为 SOCK_DGRAM。前者通常被TCP协议使用,后者通常是被UDP协议使用。
(3) protocal :指定应用程序所使用的通信协议。该参数根据第二个参数的不同而不同,第二个参数为 SOCK_STREAM,该参数为 IPPROTO_TCP ;如果第二个参数为 SOCK_DGRAM,那么该参数为 IPPROTO_UDP.
该函数的返回值是一个新创建的SOCKET 的套接字的描述符。
关闭套接字:
int closesocket( SOCKET s );
程序结束时要对Socket创建的套接字进行关闭,完成资源的释放。
参数说明如下:
s :socket() 函数创建的套接字描述符。
当创建了一个Socket后,服务器必须要绑定一个IP地址和特定的端口号。客户程序不需要绑定端口号和IP地址,因为Socket会选择合适的IP地址和端口号来使用。
绑定IP地址和端口号:
int bind( SOCKET s, const struct sockaddr FAR *name, int namelen );
参数说明如下:
(1)s :指定待绑定的Socket描述符。
(2)name :指定一个sockaddr 结构,该结构的定义如下:
struct sockaddr { u_short sa_family; char sa_data[14]; };
函数中提供的参数类型是sockaddr,在实际使用过程中,结构体是 sockaddr_in ,该结构的定义如下:
struct sockaddr_in{ short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8]; };
成语变量sin_family 设置为PF_INET ;sin_port 设置为端口号;sin_addr 结构体中只包含一个公用体,in_addr的定义如下:
该成员变量时一个整数,一般用函数inet_addr() 把字符串形式的IP地址转换为unsigned long 整型的整数值。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; };
namelen:指定name缓冲区的长度。
inet_addr函数的原型如下:
unsigned long inet_addr( const char FAR *cp );
参数cp为一个点分多进制的IP地址
inet_addr 函数的逆函数如下:
char FAR * inet_ntoa( struct in_addr in );
参数为一个addr_in 类型的变量。
监听端口:
int listen( SOCKET s, int backlog );
参数说明如下:
(1)s:使流套接字s处于监听状态。
(2)backlog :为处于监听状态的流套接字s维护一个客户连接请求队列。
接受请求:
SOCKET accept( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen );
服务端程序调用该函数从处于监听状态的流套接字的客户端请求队列中取出第一个请求并创建一个新的套接字与客户端进行通信。
参数说明如下:
(1)s:指定监听状态的套接字。
(2)addr :用来返回新创建的套接字的地址。
(3)addrlen :用来返回新创建的套接字的地址结构的长度。
连接函数如下:
int connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
客户端程序调用该函数来完成与远程服务器端的连接。
参数说明如下:
(1)s :客户端创建的套接字。
(2)name:该结构中包含了要服务器端中的IP地址和端口号。
(3)namelen:指定name缓冲区的长度。
具体进行通信的函数分为两类,一类是基于TCP协议的,一类是基于UDP协议的。数据的通信主要体现在数据的收发上,分别看一下这两种协议的收发数据的函数定义。
基于TCP的发送函数:
int send( SOCKET s, const char FAR *buf, int len, int flags );
参数说明如下:
(1)s:指定发送端套接字描述符。
(2)buf:指明一个存放应用程序要发送数据的缓冲区。
(3)len:指明实际要发送的数据的字节数。
(4)flags:一般设置为0、
基于TCP的接收函数:
int recv( SOCKET s, char FAR *buf, int len, int flags );
参数说明如下:
(1)s:指定接收端套接字描述符。
(2)buf:指定一个缓冲区,用来存放接收到的数据。
(3)len:指定缓冲区的长度。
(1)一般设置为0。
基于UDP的发送函数:
int sendto( SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, int tolen );
基于UDP的接收函数:
int recvfrom( SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR *from, int FAR *fromlen );
字节顺序:
在Socket 套接字编程中,传输数据的排列顺序以网络字节顺序和主机字节顺序为主。通常情况下,如果用户将数据通过网络发送时,需要将数据转换成以网络字节顺序排列,否则可能造成数据损坏。如果用户是将网络接收到的数据存储在本地计算机上,那么需要将数据转换成以主机字节顺序排列。
注意:IP地址结构 in_addr 中的成员S_addr 的值均是以网络字节顺序排列。
在Winsock 中提供了几个关于网络字节顺序与主机字节顺序之间的转换函数。函数定义如下:
本地字节顺序转换为网络字节顺序:
u_short htons( u_short hostshort ); u_long htonl( u_long hostlong ); unsigned long inet_addr( const char FAR *cp );unsigned long inet_addr( const char FAR *cp );//将一个字符串IP转换到以网络字节顺序排列的IP地址
网络字节顺序转换为本地字节顺序:
u_short ntohs( u_short netshort ); u_long ntohl( u_long netlong ); char FAR * inet_ntoa( struct in_addr in );//将一个以网络字节顺序排列的IP地址转换为衣蛾字符串IP
简单的通信程序:
下面我们用Windock 写一个基于TCP和UDP的“Hello World”小程序。
基于TCP协议的“Hello World !”
服务器端代码编写的流程如下:
WSAStartup() -> socket() -> bind() -> listen() -> accept() -> send() / recv() ->closesocket() -> WSACleanup()只要把这些函数依次写完,服务器端的代码就写完了。
服务器端代码如下:
#include<windows.h> #pragma comment (lib, "ws2_32")//显式连接套接字库 int main(int argc, char *argv) { WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData);//初始化套接字库 SOCKET s=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP); sockaddr_in socketaddr; socketaddr.sin_family = PF_INET; socketaddr.sin_addr.S_un.S_addr = inet_addr("127.1.1.0"); socketaddr.sin_port = htons(827); bind(s,(SOCKADDR*)&socketaddr,sizeof(SOCKADDR)); listen(s,1); SOCKADDR clientAddr; int nSize = sizeof(SOCKADDR); SOCKET clientSock; clientSock = accept(s,(SOCKADDR*)&clientAddr,&nSize); send(clientSock,"hello client \r\n",strlen("hello client \r\n")+sizeof(char),NULL); closesocket(clientSock); closesocket(s); WSACleanup();//释放套接字库 return 0; }
客户端的代码编写流程如下:
WSAStartup() -> socket() -> connect() -> send() / recv() ->closesocket() -> WSACleanup()
客户端的流程比服务端的流程要少一些,主要是省去了绑定IP和端口、监听等一些步骤。
客户端代码如下:
#include<stdio.h> #include<winsock.h> #pragma comment (lib,"ws2_32")//显示连接套接字库 int main() { WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData);//初始化套接字库 SOCKET s = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);//创建TCP套接字 sockaddr_in socketAddr; socketAddr.sin_family = PF_INET; socketAddr.sin_addr.S_un.S_addr = inet_addr("127.1.1.0"); socketAddr.sin_port = htons(827); connect(s,(SOCKADDR*)&socketAddr,sizeof(SOCKADDR)); char szBuffer[MAXBYTE] = {0}; recv(s,szBuffer,MAXBYTE,NULL); printf("szBuffer = %s \r\n",szBuffer); closesocket(s); WSACleanup();//释放套接字库 return 0; }
基于UDP协议的“Hello World !”
UDP客户端与服务器的编写方法与TCP的相似,只要主要其中的差别就行了。
服务器端代码如下:
#include<winsock.h> #include <stdio.h> #pragma comment (lib,"ws2_32") int main(int argc, char* argv[]) { WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData); SOCKET s=socket(PF_INET,SOCK_STREAM,IPPROTO_UDP); sockaddr_in sockAddr; sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); sockAddr.sin_family = PF_INET; sockAddr.sin_port = htons(827); bind(s,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR)); sockaddr_in clientAddr; int len=sizeof(sockaddr_in); char buf[MAXBYTE] = {0}; recvfrom(s,buf,MAXBYTE,0,(SOCKADDR*)&clientAddr,&len); printf("%s \r\n",buf); sendto(s,"hello world client",strlen("hello world client")+sizeof(char),0,(SOCKADDR*)&clientAddr,sizeof(SOCKADDR)); closesocket(s); WSACleanup(); return 0; }
客户端代码如下:
#include<winsock.h> #include<stdio.h> #pragma comment (lib,"ws2_32.lib") int main() { WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData); SOCKET s=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); sockaddr_in sockAddr; sockAddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); sockAddr.sin_family = AF_INET; sockAddr.sin_port = htons(827); sendto(s,"hello world server",strlen("hello world server")+sizeof(char),0,(SOCKADDR*)&sockAddr,sizeof(SOCKADDR)); sockaddr_in clientAddr; int len=sizeof(sockaddr_in); char buf[MAXBYTE] = {0}; recvfrom(s,buf,MAXBYTE,0,(SOCKADDR*)&clientAddr,&len); printf("%s \r\n",buf); closesocket(s); WSACleanup(); return 0; }