服务器端 SOCKET 编程

Posted M-Anonymous

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了服务器端 SOCKET 编程相关的知识,希望对你有一定的参考价值。

使用 Socket 的程序在使用 Socket 之前必须调用 WSAStartup() 函数,

此函数在应用程序中用来初始化 Windows Socket DLL,

只有此函数调用成功后,应用呈现才可以再调用 Windows Sockets DLL 中的其他 API 函数,

否则后面的任何函数都将调用失败。

 

WSAStartup() 函数原型:

int WSAStartup(

    WORD wVersionRequested, // 应用程序欲使用的 Windows Socket 版本号。

    LPWSADATA lpWSAData    // 指向 WSADATA 的指针。

);

函数调用后返回一个 int 型的值,通过检查这个值来确定初始化是否成功。该函数执行成功后返回 0。

 

在程序中调用该函数的形式如下:

WSAStartup( MAKEWORD(2,2), (LPWSADATA) &WSAData)

其中 MAKEWORD(2,2) 表示程序使用的是 WinSocket 2 版本,WSAData 用来存储系统传回的关于 WinSocket 的结构。

 

建立Socket:

初始化 WinSock 的动态连接库后,需要在服务器端建立一个用来监听的 Socket 句柄,为此可以调用 Socket() 函数,

用来建立这个监听的 Socket 句柄,并定义此 Socket 所使用的通信协议。

 

Socket 的原型:

SOCKET socket(

     int domain,  // 指定应用程序使用的通信协议的协议族,对于 TCP/IP 族,该函数设置为 AF_INET。

     int type,   // 指定要创建的套接字类型。

     int protocol  // 指定应用程序所使用的通信协议。

);

 

在Winsock 2 中,type 支持以下三种类型:

      SOCK_STREAM : 流式套接字。

      SOCK_DGRAM : 数据报套接字。

      SOCK_RAW:原始套接字。

在Winsock 2 中,protocol 支持以下3种类型:

     IPPROTO_UDP : UDP 协议,用于无连接数据报套接字。

     IPPROTO_TCP : TCP 协议,用于流套接字。

     IPPROTO_ICMP : ICMP 协议,用于原始套接字。

 

该函数调用成功则返回 Socket 对象,函数调用失败则返回 INVALID_SOCKET(调用 WSAGetLastError() 

可得知函数调用失败的原因,所有 WinSocket 的函数都可以使用这个函数来获取失败的原因)。

在 Windows 程序中,并不是用内存的物理地址来标志内存块,文件,任务和动态装入模块;

相反,Windows API 给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作。

 

提示:

句柄是 Windows 用来标志被应用程序所建立或使用的对象的唯一整数,Windows 使用各种各样的句柄标志,

诸如应用程序,窗口,控件,位图,GDI 对象等。一个Windows 应用程序可以用不同的方法获得一个特定项的句柄。

通常,Windows 通过应用程序的引出函数将一个句柄作为参数传递给应用程序,应用程序一旦获得了一个确定的句柄,

便可以在 Windows 环境下的任何地方对这个句柄进行操作。

 

绑定端口:

当创建了一个 Socket 以后,套接字的数据结构里有一个默认的 IP 地址和默认的 端口号。

一个服务器端程序必须调用 bind() 函数来为其绑定一个 IP 地址和一个特定的 端口号。

这样客户端才知道连接服务器的哪一个 IP 地址的哪一个 端口。

客户端程序一般不必调用 bind() 函数来为其 Socket 绑定 IP 地址和 端口号。

该函数调用成功返回 0 ,否则返回 SOCKET_ERROR.

 

 bind() 函数原型:

int bind(

    SOCKET s,      // 指定待绑定的 Socket 描述符。

    CONST struct sockaddr FAR *name,  // 指定一个 sockaddr 结构。

    int namelen   // name 结构体的大小。

);

 

这里需要简单介绍下第二个参数 name,这个参数是一个 socket 结构体类型,socket 结构类型定义如下。

struct sockaddr(

      u_short sa_family;  //指定地址族,对于 TCP/IP 族的套接字,将其设置为 AF_INET.

      char sa_data[14];

);

 

对于 TCP/IP 族的套接字进行绑定时,通常使用另一个地址结构:

struct sockaddr_in(

      short sin_family;    // 设置 TCP/IP 协议族类型 AF_INET。

      u_short sin_port;    // 指明端口号。

      struct in_addr sin_addr;   // 指明 IP 地址,该字段为整数。

                               //一般用 inet_addr()函数把字符串形式的 IP,转换成 unsigned long 型整数值后再发送给 s_addr。

      char sin_zero[8];

);

 

in_addr是一个结构体,可以用来表示一个32位的IPv4地址。

结构体里面只有一个 s_addr 变量,用来存储 IPv4 地址。

 

bind() 函数调用示例:

//...   

     struct sockaddr_in name;

     name.sin_family = AF_INET;

     name.sin_port = htonl(INADDR_ANY);

     name.sin_addr.s_addr = htonl(INADDR_ANY);

     int namelen = sizeof(name);

     bind(sSocket, (struct sockaddr *) &name, namelen);

//...

 

htonl函数:

将主机的unsigned long值转换成网络字节顺序(32位)(一般主机跟网络上传输的字节顺序是不通的,分大小端),函数返回一个网络字节顺序的数字。

网络字节序与主机字节序:

 

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,

这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。

这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

 

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,

而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,

导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。

 

记忆这类函数,主要看前面的n和后面的hl。n代表网络,h代表主机host,l代表long的长度,还有相对应的s代表16位的short。

 

以上代码中,如果不需要特别指明 IP 地址和 端口号,那么可以设定 IP 地址为 INADDR_ANY, Port 为 0。

Windows Sockets 会自动为其设定适当的 IP 地址及 Port(1024~5000)。

如果想得到 IP 地址和 端口号,可以调用 getsockname() 函数来获知其被设定的值。

 

监听端口:

服务器端的 Socket 对象绑定完成后,服务端必须建立一个监听的队列,来接收客户端发来的连接请求。

listen() 函数使服务器端的 Socket 进入监听状态,并设定可以建立的最大连接数。

 

listen() 函数原型:

int listen(

     SOCKET S,       // 指定监听的 Socket 描述符。

     int backlog     // 为一次同时连接的最大数目(不可超过 5)

);

该函数调用成功后返回 0,否则返回 SOCKET_ERROR。服务器端的 Socket 调用完 listen() 函数后,

使其套接字 s 处于监听状态,处于监听状态的流套接字 s 将维护一个客户连接请求队列。最多容纳 backlog 个客户请求。

 

创建服务器端接收客户端请求:

当客户端发出连接请求时,服务器端 hwnd 视窗会收到 Winsock stack 送来自定义的一个消息。

为了使服务器端接收客户端的连接请求,就要使用 accept() 函数。处于监听状态的流套接字 s 

从客户端连接请求队列中取出排在最前面的一个客户请求,并且创建一个新的套接字来与客户端

套接字共同创建连接通道。原来处于监听状态的套接字继续处于监听状态,等待客户端的连接,

这样服务器端和客户端才算正式完成通信程序的连接动作。如果创建连接通道成功,

就返回新创建套接字的描述符,以后与客户端套接字交换数据的是新创建的套接字;

如果失败就返回 INVALID_SOCKET。

 

accept() 函数原型:

SOCKET accept(

      SOCKET s,         // 处于监听状态的流套接字。

      struct sockaddr FAR *addr, // 用来返回新创建的套接字的地址结构。

      int FAR *addrlen    // 指明用来返回新创建的套接字的地址结构的长度。

);

accept() 函数示例:

//...

     struct sockaddr_in addr;

     int addrlen;

     addrlen = sizeof(addr);

     accept(sSocket,(struct sockaddr *)&addr,&addrlen);

//...

 

服务器端响应客户端连接请求:

当客户端向服务器端发出连接请求,服务端即可用 accept() 函数实现与客户端建立连接,为了达到

服务端的 Socket 在恰当的时候与从客户端发来的连接请求建立连接,服务端需要使用 WSAAsyncSelect() 函数,

让系统主动来通知服务器端程序有客户端提出连接请求了。

该函数调用成功返回 0,否则返回 SOCKET_ERROR。

 

WSAAsyncSelect() 函数原型:

int WSAAsySelect(

    SOCKET s,         // Socket 对象。

    HWND hwnd,        // 接收消息的窗口句柄。

    unsigned int wMsg,  // 传给窗口的消息。

    long lEvent   // 被注册的网络事件,即是应用程序向窗口发送消息的网络事件。

);

lEvent 值为下列组合:

 

          FD_READ:希望在套接字 S 收到数据时收到消息。

 

          FD_WRITE:希望在套接字 S 发送数据时收到消息。

 

          FD_ACCEPT:希望在套接字 S 收到连续请求时收到消息。

 

          FD_CONNECT:希望在套接字 S 连接成功时收到消息。

 

          FD_CLOSE:希望在套接字 S 连接关闭时收到消息。

 

          FD_OOB:希望在套接字 S 收到带外数据时收到消息。

 

该函数在具体应用时,wMsg 应是在应用程序中定义的消息名称,而消息结构中的 IParam 则为以上各种网络事件名称。

 

所以,可以在窗口处理自定义消息函数中使用以下结构代码来响应 Socket 的不同事件。

switch(lParam)
{
    case FD_READ:
     ......
     break;
    case FD_WRITE:
     ......
     break;
    case FD_ACCEPT:
     ......
     break;
//...
}

 

完成服务端与客户端 Socket 连接:     

结束服务器端与客户端的通信连接,可以由服务器端或客户端的任一端发出请求,只要调用 closesocket() 就可以了。

同样的,要关闭服务器端监听状态的 Socket,也是利用该函数。在调用 closesocket() 函数关闭 Socket 之前,

与程序启动时调用 WSAStartup() 函数相对应。程序结束前,需调用 WSACleanup() 来通知  Winsock Stack释放 Socket 所占用的资源。

该函数调用成功后返回 0,失败返回 SOCKET_ERROR。

 

WSACleanup()函数原型:

int WSACleanup();

该函数在应用程序完成对请求的 Socket 库的使用后调用,来解除与 Socket 库的绑定并且释放 Socket 库

所占用的资源。该函数一般用在网络程序结束的地方调用。

 

closesocket() 函数原型:

int closesocket(

    socket s    // 表示关闭的套接字。

);

该函数如果成功执行就返回 0,否则返回 SOCKET_ERROR。

每个进程中都有一个套接字描述表,表中的每个套接字描述符都对应了一个位于操作系统缓存区的套接字数据结构。 

因此,可能有几个套接字描述符指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构被引用的次数,

即有多少个套接字描述符指向该结构。当调用 closesocket() 函数时,操作系统先检查套接字数据结构中的该字段的值。

如果值为 1,就表明只有一个套接字描述符指向它,因此操作系统就先把 s 在套接字描述符表中对应的那条表项清除,

并且释放 s 对应的套接字数据结构;如果该字段值大于 1,那么操作系统仅仅清除 s 在套接字描述符表中的对应表项,

并且把 s 对应的套接字数据结构的引用次数减 1。


以上是关于服务器端 SOCKET 编程的主要内容,如果未能解决你的问题,请参考以下文章

Socket编程的简单实现

socket网络编程:客户端与服务端代码bug修复(代码完善)

Python socket编程客户端与服务端通信

socket编程模拟linux下的ssh代码实现

socket 编程 服务器端-客户端

C语言 UDP socket 简单客户端 编程,急