win32 socket 编程(三)——TCP/IP

  一、TCP/IP解析

TCP/IP协议的核心部分是传输层协议(TCP、UDP),网络层协议(IP)和物理接口层,这三层通常是在操作系统内核中实现。因此用户一般不涉及。编程时,编程界面有两种形式:

  1.1、是由内核直接提供的系统调用;

  1.2、使用以库函数方式提供的各种函数。

  前者为核内实现,后者为核外实现。用户服务要通过核外的应用程序才能实现,所以要使用套接字(socket)来实现。

二、TCP/IP服务器及客户端操作流程

2.1服务器操作流程

  2.1.1 加载套接字库。在初始化阶段调用 WSAStartup()

此函数在应用程序中初始化 Windows Sockets DLL,只有此函数调用成功后,应用程序才可以再调用其他Windows Sockets DLL 中的 API 函数。该函数原型

int WSAStartup(WORD wVersionRequested,  LPWSADATA lpWSAData);

参数:

   wVersionRequested:Socket的版本号;

  WORD 类型、MAKEWORD、LOBYTE、HIBYTE 宏  ;

  WORD 类型是一个 16 位的无符号整型, 在 WTYPES.H 中被定义为:  typedef unsigned short  WORD;  

其目的是提供两个字节的存储, 在 Socket 中这两个字节可以表示主版本号和副版本号。  使用 MAKEWORD 宏可以给一个 WORD 类型赋值。例如要表示主版本号 2,副版本号 0,可以使用如下代码:  

 

 WORD wVersionRequested;  

 wVersionRequested = MAKEWORD(2, 0);  

 

注意:低位内存存储主版本号2,高位内存存储副版本号0,其值为 0x0002。  使用宏 LOBYTE 可以读取 WORD 的低位字节HIBYTE 可以读取 WORD 的高位字节。    lpWSAData:指向一个用于存储 Socket 库信息的WSAStartup结构。

返回值:

  a) 等于0,初始化成功;

  b)不等于0, 初始化失败;

  2.1.2 创建套接字 Socket

  初始化 WinSockt 的动态连接库后,需要在服务器端建立一个监听的 Socket,为此可以调用 Socket()函数用来建立这个监听的 Socket,并定义此 Socket 所使用的通信协议.此函数调用成功返回 Socket 对象,失败则返回 INVALID_SOCKET.调用 WSAGetLastError()可得知原因,所有 WinSocket 的 API 函数都可以使用这个函数来获取失败的原因。函数原型如下:

SOCKET socket(int af,int type,int protocol)

参数:

a)af: 代表网络地址族,目前只有一种取值有效,即 AF_INET, 代表 internet 地址族。

b) type: 代表网络协议类型, SOCK_DGRAM 代表 UDP 协议, SOCK_STREAM 代表 TCP 协议。   

c) protocol: 指定网络地址族特殊协议,目前无用,赋值0即可。

如果要建立的是遵从 TCP/IP 协议的 socket,第二个参数 type 应为 SOCK_STREAM,如为 UDP(数据报)的socket,应为 SOCK_DGRAM。sockets(套接字)编程有三种:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。基于 TCP 的 socket 编程是采用的流式套接字。

返回值:SOCKET 类型

SOCKET 是 socket 套接字类型,在 WINSOCK2.H 中有如下定义:  

typedef unsigned u_int;  

typedef u_int    SOCKET;  

可知套接字实际上就是一个无符号整形,它将被 Socket 环境管理和使用。  套接字将被创建、设置、用来发送和接收数据,最后会被关闭。 

  2.1.3绑定端口(blind)

  将创建的套接字绑定到本机地址的某一端口上,接下来要为服务器端定义的这个监听的 Socket 指定一个地址及端口(Port),这样客户端才知道待会要连接哪一个地址的哪个端口,为此要调用 bind()函数,该函数调用成功返回 0,否则返回 SOCKET_ERROR.

int bind( SOCKET s,const struct sockaddr FAR *name,int namelen);

参 数:

a)s:被绑定的套接字

b) name: 是一个sockaddr结构指针,该结构中包含了要绑定的地址和端口。

c) namelen:第二个参数name 的长度;

  如果使用者不在意地址或端口的值,那么可以设定地址为 INADDR_ANY,及 Port 为 0。对于多接口主机使用INADDR_ANY指定了一个通配地址,让该主机的任何一个IP地址都匹配。Windows Sockets会自动将其设定适当之地址及 Port(1024 到 5000 之间的值)。此后可以调用 getsockname()函数来获知其被设定的值。

下面对其涉及的类型作一番解析:  

(1) sockaddr_in类型: 

sockaddr_in 定义了socket发送和接收数据包的地址,其定义如下:  

strucrt  sockaddr_in  

{  

    short       sin_family;  

    u_short      sin_port;  

    struct in_addr  sin_addr;  

    char        sin_zero[8];  

};  

其中 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;  

};  

首先阐述 in_addr 的信义。  很显然它是一个存储 ip 地址的联合体,有三种表达方式:  

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

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

c) 第三种用一个长整型来表示IP地址;  

给 in_addr 赋值的一种最简单方法是使用 inet_addr 函数,它可以把一个代表IP地址的字符串赋值。转换为in_addr类型。如:  

addrServer.sin_addr = inet_addr("192.168.0.2");  

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

sockaddr_in的含义比in_addr的含义要广泛,其各个字段的含义和取值如下:  

a)第一字段 short sin_family,代表网络地址族,如前所述,只能取值AF_INET;  

b) 第二字段 u_short  sin_port,代表IP地址端口,由程序员指定;  

c) 第三字段 struct in_addr  sin_addr,代表IP地址;  

d)第四个字段char sin_zero[8],是为了保证sockaddr_in与SOCKADDR类型的长度相等而填充进来的字段。  

(2) sockaddr 类型

sockaddr 类型是用来表示 Socket 地址的类型,同 socketaddr_in 类型相比,sockaddr 的适用范围更广。

TCP/IP 地址。sockaddr 的定义如下:

struct sockaddr  
{  
     ushort  sa_family;  
     char    sa_data[14];  
}; 

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

  2.1.4为套接字设置监听模式,准备客户请求。

  当服务器端的 Socket 对象绑定完成之后,服务器端必须建立一个监听的队列来接收客户端的连接请求.listen()函数使服务器端的 Socket 进入监听状态,并设定可以建立的最大连接数(目前最大值限制为 5,最小值为 1).该函数调用成功返回 0,否则返回 SOCKET_ERROR。

int listen(SOCKET s,int backlog );

:

a) s:需要建立监听的 Socket;

b) backlog:最大连接个数;

  2.1.5  接受连接请求

  当 Client 提出连接请求时,Server 端 hwnd 视窗会收到 Winsock Stack 送来自定义的一个消息,这时可以分析 lParam,然后调用相关的函数来处理此事件。为了使服务器端接受客户端的连接请求,就要使用accept()函数,该函数新建一 Socket 与客户端的 Socket 相通,原先监听之 Socket 继续进入监听状态,等待他人的连接要求。该函数调用成功返回一个新产生的 Socket 对象,否则返回 INVALID_SOCKET。

SOCKET accept(SCOKET s,struct sockaddr FAR *addr,int FAR *addrlen );

参数:

a) s: 监听套接字

b) addr:存放来连接的客户端的地址、端口信息;

c) addrlen:addr 的长度

  2.1.6  新返回的套接字和客户端进行通信。send/recv

send函数通过一个已建立连接的套接字发送数据,函数声明如下:

int send(SOCKET s, const char FAR *buf, int len, int flags);

参数:

a) s: 是一个已建立连接的套接字。

b) buf:指向一个缓冲区,该缓冲区包含将要传递的数据。

c) len:缓冲区的长度。

d) flags:收发数据方式的标识,如果不需要特殊要求可以设置为0。

调用send函数向客户端发送数据,注意这个函数使用的套接字需要使用已建立连接的那个套接字,而不是用于监听的那个套接字。

recv函数从一个已连接的套接字接收数据。函数原型如下:

int recv(SOCKET s,char FAR* buf, int len, int flags);

参数:

a) s:建立连接之后准备接收数据的那个套接字。

b) buf:指向缓冲区的指针,用来保存接收的数据。

c)len:缓冲区的长度。

d)flags:收发数据方式的标识,如果不需要特殊要求可以设置为0。

发送完数据之后还可以从客户端接收数据,这可以使用recv函数,应注意该函数的第一个参数也应该是建立连接之后的那个套接字并且定义一个字符数组recvBuf,用来保存接收的数据。

  2.1.6 关闭套接字

  结束服务器和客户端的通信连接是很简单的,这一过程可以由服务器或客户机的任一端启动,只要调用closesocket()就可以了,而要关闭 Server 端监听状态的 socket,同样也是利用此函数.另外,与程序启动时调用 WSAStartup()函数相对应,程式结束前,需要调用 WSACleanup()来通知 Winsock Dll 释放 Socket 所占用的资源.这两个函数都是调用成功返回 0,否则返回 SOCKET_ERROR

int PASCAL FAR closesocket( SOCKET s );

参数:

s:Socket 的识别码;

int PASCAL FAR WSACleanup( void );

 

posted on 2019-06-16 15:57  lfylcj  阅读(912)  评论(0编辑  收藏  举报

导航