首页 > 代码库 > 【转】Windows socket基础

【转】Windows socket基础

转自:http://blog.csdn.net/ithzhang/article/details/8448655

 

  Windows socket 基础

 

     Windows socket是一套在Windows操作系统下的网络编程接口。它不是一种网络协议,而是一个开放的、支持多个协议的Windows下的网络编程接口

     Windows socket是以Unix socket为基础,因此Windows socket中的许多函数名与Unix都是一样的。除此之外它还允许开发人员充分利用Windows的消息驱动机制进行程序设计开发。

 

     套接字是应用层到运输层的接口。套接字用以表示一条连接的两端。每一个端点由ip和端口组成。因此套接字是由两端点的ip和端口组成。

 

     端口是运输层的概念,每个端口对应一个进程。因此一条连接表示一个进程与另一个进程建立联系。

 

     应用程序可以使用两种套接字。流套接字和数据包套接字。分别对应TCP和UDP。

 

     TCP提供面向连接的可靠的、无重复、有序的数据流服务。而UDP提供面向数据包的,不保证数据是可靠的、有序的和无重复的。

     在Windows环境下,使用Windows socket api进行网络程序开发时,需要调用Windows操作系统的Windows socket动态库。在应用程序中需要包含Windows sockets头文件。windows sockets 2.2版本需要包含WINSOCK2.h头文件(不区分大小写)。同时还需要添加动态库。一种是在头文件中添加。如:

     #pragma  comment(lib,"WS2_32.lib")

     另一种是在vc中添加。可以选择project -》settting,在link标签下添加wsock32.lib字符串。

 

     Windows sockets中定义的套接字类型SOCKET来表示套接字:

 

[cpp] view plaincopy
 
  1. typedef unsigned int u_int;  
  2.   
  3. typedef u_int SOCKET;  


 

 

      其实所谓的SOCKET的类型只不过是unsigned int的别名罢了。INVALID_SOCKET表示一个无效的套接字,除此之外的0--INVALID_SOCKET-1都表示一个有效的套接字。因此在创建套接字后,都需要与INVALID_SOCKET比较,看创建的套接字是否有效。

[cpp] view plaincopy
 
  1. SOCKET s=socket(...);  
  2.   
  3. if(INVALID_SOCKET==s)  
  4.   
  5. {  
  6.   
  7.   //创建失败。  
  8.   
  9. }  


 

 

     Windows SOCKET可以支持多种不同的网络协议,并且提供与协议无关的编程接口。因此开发人员就可以相同的格式开发使用任一协议的网络应用程序,而不去关心各种协议的不同。

 

     每种协议都有一套不同的IP定址方案(即表示主机地址的方式)。TCP协议和UDP协议通过IP协议传输数据。

 

     而Windows SOCKET通过AF_INET地址家族为IP协议定址。

 

 

[cpp] view plaincopy
 
  1. #define AF_INET 2  


 

     网络中每台主机都有一个IP地址,用32位数字来表示。TCP和UDP必须指定端口号。在Windows SOCKET中sockaddr_in 结构被用来指定IP和端口号。

 

 

[cpp] view plaincopy
 
  1. struct sockaddr_in  
  2.   
  3. {  
  4.   
  5.    short sin_family;  
  6.   
  7.    u_short sin_port;  
  8.   
  9.    struct in_addr sin_addr;  
  10.   
  11.    char sin_zero[8];  
  12.   
  13. };  


 

     sin_family表示地址家族。使用TCP/IP协议的应用程序必须为aF_INET,来告诉系统使用IP地址家族 。

     sin_port指定服务的端口号。1024--49151范围内的数据被作为服务端口号,可以由用户自定义。    sin_zero字段作为填充字段。以便使得该结构与SOCKADDR结构长度相同。

in_addr的定义如下:

 

[cpp] view plaincopy
 
  1. struct in_addr {         
  2.   
  3.          union {                  
  4.   
  5.                 struct{  
  6.   
  7.                         u_char s_b1,s_b2,s_b3,s_b4;   
  8.   
  9.                        }S_un_b;         
  10.   
  11.                 struct {  
  12.   
  13.                        u_short s_w1,s_w2;   
  14.   
  15.                       } S_un_w;    
  16.   
  17.                 u_long S_addr;     
  18.   
  19.              } S_un;  
  20.   
  21.           }  


 

     很显然它是一个存储ip地址的联合体,有三种表达方式:

 

     第一种用四个字节来表示IP地址的四个数字;

     第二种用两个双字节来表示IP地址;

     第三种用一个长整型来表示IP地址。

 

     给in_addr赋值的一种最简单方法是使用inet_addr函数,它可以把一个代表IP地址的字符串赋值转换为in_addr类型,如addrto.sin_addr.s_addr=inet_addr("192.168.0.2");

 

     其反函数是inet_ntoa,可以把一个in_addr类型转换为一个字符串。

 

      sockaddr类型sockaddr类型是用来表示socket地址的类型,同上面的sockaddr_in类型相比,sockaddr的适用范围更广,因为sockaddr_in只适用于TCP/IP地址。

 

sockaddr的定义如下:

 

 

[cpp] view plaincopy
 
  1. struct sockaddr {   
  2.   
  3.        u_short sa_family;  
  4.   
  5.       char  sa_data[14];  
  6.   
  7. };    


 

     可知sockaddr有16个字节,而sockaddr_in也有16个字节,所以sockaddr_in是可以强制类型转换为sockaddr的。事实上也往往使用这种方法。

     不同cpu处理多字节时处理方式不同。Intel x86cpu对多字节的处理方式为高对高低对低。但是在网络上采用的是高对低,低对高的方式。因此也就存在所谓主机字节序和网络字节序的处理问题。

 

     Htonl和htons函数实现主机字节顺序和网络字节序的转换功能。H代表host,主机。N代表net。L代表long。S代表short。不要使用htonl转换short哦,楼主就有一次犯了这个错误,找了好久才找到原因。

 

当然也有从网络字节序到主机字节序的转换函数:ntohl和ntohs。

 

     除了支持TCP/IP协议之外,Windows SOCKET还支持IPX/SPX、ATM和红外线通信协议等等,它们都有自己的定址方法,感兴趣的同学可以参考其他资料。

 

     基本的套接字编程

     在使用套接字进行编程之前,无论是服务器还是客户端都必须加载Windows SOCKET动态库。函数WSAStartup就实现了此功能。它是套接字应用程序必须调用的第一个函数。

 

 

[cpp] view plaincopy
 
  1. int WSAStartup(  
  2.   
  3.    WORD wVersionRequested,  
  4.   
  5.    LPWSADATA lpwsadata);  


 

     第一个参数指定准备加载的Windows SOCKET动态库的版本。一般使用宏MAKEWORD构造。如MAKEWORD(2,2)表示加载2.2版本。

 

      WSADATA会返回被加载动态链接库的基本信息。如是否加载成功,以及当前实际使用的版本。具体结构不再介绍。

 

     初始化socket之后,需要创建套接字。socket函数和WSASocket函数可以实现此功能。

 

 

[cpp] view plaincopy
 
  1. SOCKET socket(  
  2.   
  3.                int af,  
  4.   
  5.                int type,  
  6.   
  7.                int protocao);  


 

af表示使用协议的地址家族。创建TCP或UDP的套接字是使用AF_INET。

 

type表示套接字的类型。有SOCK_STREAM、SOCK_DGRAM和SOCK_RAM三种类型。分别表示流、数据包、原始套接字。

 

protocol,指示使用的协议。对于SOCK_STREAM套接字类型,该字段可以为IPPROTO_TCP或0。对于SOCK_DGRAM套接字类型,该字段为IPPROTO_UDP或0。

 

当函数创建成功时,返回一个新建的套接字句柄。否则将返回INVALID_SOCKET。如

 

 

[cpp] view plaincopy
 
  1. SOCKET s=socket(AF_INET,SOCK_STREAM,0);  
  2.   
  3. if(INVALID_SOCKET)  
  4.   
  5. {  
  6.   
  7.    //失败。  
  8.   
  9. }  


 

bind函数

 

在创建套接字之后就需要调用bind函数将其绑定到一个已知的地址上。

 

 

[cpp] view plaincopy
 
  1. int bind(SOCKET s,  
  2.   
  3.        const struct sockaddr*name,  
  4.   
  5.        int namlen);  


 

s为要绑定的套接字,name为要绑定的地址。namelen为sockaddr长度。

 

函数调用成功将返回0,否则返回值为SOCKET_ERROR。如果程序不关心分配给它的地址,可使用INADDR_ANY或将端口号设为0。端口号为0时,Windows SOCKET将给应用程序分配一个值在1024-5000之间唯一的端口号。

 

示例:

 

[cpp] view plaincopy
 
  1. struct sockaddr_in addr;  
  2.   
  3. int nServerPort=5500;  
  4.   
  5. int nErrorCode;  
  6.   
  7. addr.family=AF_INET;  
  8.   
  9. addr.port=htons(nServerPort);  
  10.   
  11. addr.sin_addr.S_addr=htonl(INADDR_ANY);  
  12.   
  13. SOCKET s=socket(AF_INET,SOCK_STREAM,0);  
  14.   
  15. if(s==INVALID_SOCKET)  
  16.   
  17. {  
  18.   
  19.   //失败。  
  20.   
  21. }  
  22.   
  23. nErrorCode=bind(s,(SOCKADDR*)&addr,sizeof(addr));  
  24.   
  25. if(nErrorCode==SOCKET_ERROR)  
  26.   
  27. {  
  28.   
  29.   //失败。  
  30.   
  31.   
  32.   
  33. }  


 

listen函数将套接字设定为监听模式。

 

 

[cpp] view plaincopy
 
  1. int listen (  
  2.   
  3.    SOCKET s,  
  4.   
  5.    int backlog);  


 

s为要设置监听模式的套接字。

backlog指定等待连接的最大队列长度。

当函数成功时返回0,否则返回SOCKET_ERROR。

假如backlog为3,则说明最大等待连接的最大值为3.如果有四个客户端同时向服务器发起请求,那么第四个连接将会发生WSAEWOULDBLOCK错误。当服务器接受了一个请求,就将该请求从请求队列中删去。

 

 

[cpp] view plaincopy
 
  1. int ret=listen(s,3);  
  2.   
  3. if(ret==SOCKET_ERROR)  
  4.   
  5. {  
  6.   
  7.    //失败。  
  8. }  


 

accept函数接受客户端的一个连接请求。

 

 

[cpp] view plaincopy
 
  1. SOCKET accept{  
  2.   
  3.      SOCKET s,  
  4.   
  5.      struct sockaddr*addr,  
  6.   
  7.      int *addrlen);  


 

s为监听套接字。

 

addr返回客户端地址。

 

addrlen返回addr的长度。

 

函数执行成功时:

 

1:主机接受了等待队列的第一个请求。

 

2:addr结构返回客户端地址。

 

3:返回一个新的套接字句柄。服务器可以使用该套接字与客户端进行通信。而监听套接字仍用于接受客户端连接。

 

[cpp] view plaincopy
 
  1. SOCKET sListen;//监听套接字。  
  2.   
  3. SOCKET sAccept;//接受套接字。  
  4.   
  5. sockaddr_in addrClient;//客户端地址。  
  6.   
  7. int addrClientLen=sizeof(addrClient);  
  8.   
  9. sAccept=accept(sListen,(SOCKADDR*)&addrClient,&addrClientLent);  
  10.   
  11. if(INVALID_SOCKET==sAccept)  
  12.   
  13. {  
  14.   
  15.    //失败。  
  16.   
  17. }  


 

 

recv()函数

recv()和WSARecv()函数用于接收数据。

 

[cpp] view plaincopy
 
  1. int recv(  
  2.   
  3.       SOCKET s,  
  4.   
  5.       char *buf,  
  6.   
  7.       int len,  
  8.   
  9.       int flags);  


 

s为接收数据套接字。

 

buff接受缓冲区。

 

len缓冲区长度。

 

flags:该参数影响函数的行为。它可以是0,MEG_PEEK和MSG_OOB。0表示无特殊行为。MSG_PEEK会使有用的数据被复制到buff中,但没有从系统缓冲区内将这些数据删除。MSG_OOB表示处理外带数据。

 

recv函数返回接收的字节数。当函数执行失败时返回SOCKET_ERROR。

 

 

[cpp] view plaincopy
 
  1. SOCKET s;  
  2.   
  3. char buff[128];  
  4.   
  5. nReadlen=recv(s,buff,128,0);  
  6.   
  7. if(SOCKET_ERROR==nReadlen)  
  8.   
  9. {  
  10.   
  11. }  


 

send函数

send()和WSASend()用于发送数据。

 

 

[cpp] view plaincopy
 
  1. int send(  
  2.   
  3.     SOCKET s,  
  4.   
  5.     const char *buff,  
  6.   
  7.     int len,  
  8.   
  9.     int flags);  


 

s为发送套接字。

 

buff为发送缓冲区。

 

len发送数据长度。

 

flags可以是0或MSG_DONTROUTE或MSG_OOB。0表示无特殊行为。MSG_DONTROUTEyaoqiu传输层不要将此数据路由出去。MSG_OOB表示该数据应该被外带发送。

 

函数成功时返回实际发送的字节数。失败返回SOCKET_ERROR。

 

 

[cpp] view plaincopy
 
  1. SOCKET s;  
  2.   
  3. char buff[128];  
  4.   
  5. int ret;  
  6.   
  7. strcpy(buff,”sendData”);  
  8.   
  9. ret=send(s,strlen(buff),0);  
  10.   
  11. if(SOCKET_ERROR==ret)  
  12.   
  13. {  
  14.   
  15. }  


 

closesocket()函数

 

closesocket()函数用以关闭套接字。释放套接字所占资源。

 

 

[cpp] view plaincopy
 
  1. int closesocket(  
  2.   
  3.     SOCKET s);  


 

调用过closesocket函数的套接字继续使用时会返回WSAENOTSOCK错误。

 

shutdown()函数。

 

Shutdown()函数用于通知对方不再发送数据或者不再接收数据或者既不发送也不接收数据。

 

 

[cpp] view plaincopy
 
  1. int shutdown(  
  2.   
  3.    SOCKET s,  
  4.   
  5.    int how);  


 

how参数可以是:

SD_RECEIVE表示不再接收数据,不允许再调用接收数据函数。

SD_SEND表示不再发送数据。

SD_BOTH表示既不发送也不接收数据。

 

 

connect函数实现连接服务器的功能。

 

[cpp] view plaincopy
 
  1. int connect(  
  2.   
  3.        SOCKET s,  
  4.   
  5.        const struct sockaddr*name,  
  6.   
  7.        int namelen);  


    s为套接字。

    name为服务器地址。

    namelen为sockaddr结构长度。

    函数执行成功返回0,否则返回SOCKET_ERROR。

 

 

[cpp] view plaincopy
 
  1. SOCKET s;  
  2.   
  3. ULONG ulServIp;  
  4.   
  5. USHORT ServPort;  
  6.   
  7. int ret;  
  8.   
  9. SOCKADDR_IN servAddr;  
  10.   
  11. servAddr.sin_family=AF_INET;  
  12.   
  13. servAddr.sin_addr.S_addr=htonl(ulServIp);  
  14.   
  15. servAddr.sin_port=htons(ServPort);  
  16.   
  17. int len=sizeof(servaddr);  
  18.   
  19. ret=connect(s,(SOCKADDR*)&servAddr,sizeof(servAddr));  
  20.   
  21. if(ret==SOCKET_ERROR)  
  22.   
  23. {  
  24. }  


 

     创建套接字后,可以对它的各种属性进行设置。可以调用getsockopt()函数来返回套接字选项信息。setsockopt()设置套接字选项。

 

getsockopt()函数

 

它用于获取套接字选项信息:

 

 

[cpp] view plaincopy
 
  1. int getsockopt(  
  2.   
  3. SOCKET s,  
  4.   
  5. int level,  
  6.   
  7. int optname,  
  8.   
  9. char *optval,  
  10.   
  11. int optlen);  


 

s为要取得选项的套接字。

level为选项级别,有SOL_SOCKET和IPPROTO_TCP两个级别。

optname套接字选项名称。

optval该参数返回套接字选项名称对应的值。

optlen为缓冲区optval大小。

函数成功,返回值为0。否则返回SOCKET_ERROR。

 

[cpp] view plaincopy
 
  1. int Bufflen;  
  2.   
  3. int noptlen=sizeof(nBufflen);  
  4.   
  5. int ret=getsockopt(s,SOL_SOCKET,SO_RCVBUF,(char*)&BuffLen,&noptlen);  
  6.   
  7. if(ret==SOCKET_ERROR)  
  8.   
  9. {  
  10.   
  11. }  


 

setsockopt函数。

 

它可以设置套接字选项。若不能正确设置socket属性,则数据的发送和接收会失败

 

 

[cpp] view plaincopy
 
  1. int setsockopt(  
  2.   
  3.               SOCKET s,  
  4.   
  5.               int level,  
  6.   
  7.               int optname,  
  8.   
  9.               char *optval,  
  10.   
  11.               int optlen);  


 

s为要取得选项的套接字。

 

level为选项级别,有SOL_SOCKET和IPPROTO_TCP两个级别。

optname套接字选项名称。

optval参数设置套接字选项的值。

optlen为optval大小。

 

下列代码首先调用getsockopt函数获得默认接收缓冲区的大小,然后调用setsockopt将接收缓冲区大小设置为原来的10倍。再次调用getsockopt来检查是否设置成功。

 

 

[cpp] view plaincopy
 
  1. int opt;  
  2.   
  3. int noptlen=sizeof(opt);  
  4.   
  5. int ret=getsockopt(s,SOL_SOCKET,SO_RECVBUFF,(char*)&opt,noptlen);  
  6.   
  7. if(ret==SOCKET_ERROR)  
  8.   
  9. {  
  10.   
  11. }  
  12.   
  13.   
  14. opt*=10;  
  15.   
  16. ret=setsockopt(s,SOL_SOCKET,SO_RECVBUFF,(char*)&opt,noptlen);  
  17.   
  18. if(ret==SOCKET_ERROR)  
  19.   
  20. {  
  21.   
  22. }  
  23.   
  24. int newopt;  
  25.   
  26. getsockopt(s,SOL_SOCKET,SO_RECVBUFF,(char*)&newopt,&noptlen);  
  27.   
  28. if(newopt!=opt)  
  29.   
  30. {  
  31.   
  32.   //设置失败。  
  33. }  


 

当设置SOL_SOCKET选项级别时,调用setsockopt函数和getsockopt函数所设置或获取的信息为套接字本身的特征,这些信息与基层协议无关。

 

SOL_SOCKET级别包括一下类型:

SO_ACCEPTCONN      bool类型。如果为真,表明套接字处于监听模式。

SO_BROADCAST       bool类型,如果为真,表明套接字已设置成广播消息发送。

SO_DEBUG           bool类型。如果为真,则允许输出调试信息。

SO_DONTLINGER      bool类型。如果为真,则禁止SO_LINGER。

SO_DONTROUTE       bool类型。如果为真,则不会做出路由选择。

SO_ERROR           int 类型。返回和重设以具体套接字为基础的错误代码。

SO_KEEKPALIVE       bool类型。如果为真,则套接字在会话过程中会发送保持活动消息。

SO_LINGER           struct linger*类型,设置或获取当前的拖延值。

SO_OOBINLINE        bool如果为真,则带外数据会在普通数据流中返回。

SO_RECVBUF          int类型。设置或获取接收缓冲区长度。

SO_REUSEADDR        bool类型。如果为真,套接字可以与一个正在被其他套接字使用的地址绑定。

SO_SNDBUF           bool类型。设置或获取发送缓冲区大小。

SO_TYPE            int类型。返回套接字类型,如SOCK_DGREAM,SOCK_STREAM。

SO_SNDTIMEO         int类型。设置或获取套接字在发送数据的超时时间。

SO_RECVTIMEO        int类型,设置或获取套接字在接收数据的超时时间。

 

     WSAGetLastError函数
      该函数用来在Socket相关API失败后读取错误码,根据这些错误码可以对照查出错误原因。

        GetComputerName()用来获取计算机名称:

[cpp] view plaincopy
 
  1. BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD lpnSize);  
  2.   
  3. plBuffer是用来存储返回的名称的缓冲区。  

     lpBuffer为缓冲区。

     lpnSize为缓冲区大小。同时它也返回计算机名称的长度。

     此函数不属于winsock库函数。使用之前不需要初始化库。

     使用方法为:

[cpp] view plaincopy
 
  1. <span style="font-size:18px;">    TCHAR szHostName[20];  
  2.     DWORD dwSize = 20;  
  3.     GetComputerName( szHostName, &dwSize );  
  4.   
  5. </span>  

 

     gethostname函数:

     此函数为WinSock库函数,使用之前需要初始化WinSock库。

[cpp] view plaincopy
 
  1. int  gethostname(char  *name, int namelen);  

     name为存储主机名缓冲区。

     namelen为缓冲区长度。

以上参考自《Windows sockets网络开发--基于Visual C++实现》如有纰漏,请不吝赐教!!!

 

写了个例子:http://files.cnblogs.com/cappuccino/Socket.rar