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 );