1.2 获取网络中计算机的IP地址和计算机名
在开发网络应用的过程中,经常需要获取网络中某台计算机的IP地址和计算机名称。在本节的内容中,将介绍如何使用Visual C++ 6.0开发一个实现上述功能的应用程序。
1.2.1 流式套接字编程(1)
网络数据的传输是通过套接字实现的。套接字有3种类型:流式套接字(SOCK_ STREAM),数据报套接字(SOCK_DGRAM)及原始套接字(RAW)。在本小节的内容中,将首先讲解流式套接字编程的基本知识。
流式套接字是面向连接的,提供双向、有序、无重复且无记录边界的数据流服务,适用于处理大量数据,可靠性高,但开销也大,编程模型如图1-11所示。
(点击查看大图)图1-11 流式套接字编程模型 |
1.服务器端编程步骤
(1) 在初始化阶段调用函数WSAStartup()
此函数在应用程序中初始化Windows Sockets DLL,只有此函数调用成功后,应用程序才可以再调用其他Windows Sockets DLL中的API函数。
在程序中该函数的调用形式如下:
- int WSAStartup(
- WORD wVersionRequested, //所使用WinSocket版本
- LPWSADATA lpWSAData //存储系统返回的WinSocket信息
- );
(2) 建立Socket
初始化WinSock的动态链接库后,需要在服务器端建立一个监听Socket,为此可以调用socket()函数来建立这个监听的Socket,并定义此Socket所使用的通信协议:
- SOCKET socket(
- int af, //目前只提供PF_INET(AF_INET)
- int t ype, //Socket的类型(SOCK_STREAM、SOCK_DGRAM)
- int protocol //通讯协议(如果使用者不指定则设为0)
- );
调用成功返回Socket对象,失败则返回INVALID_SOCKET(调用WSAGetLastError()可得知原因,所有WinSocket的函数都可以使用这个函数来获取失败的原因)。
如果要建立的是遵从TCP/IP协议的Socket,第二个参数type应为SOCK_STREAM,如为UDP(用户数据报协议)的Socket,type应为SOCK_DGRAM。
(3) 绑定端口
接下来要为服务器端定义的监听Socket指定一个地址及端口(Port),这样客户端才知道待会儿要连接哪一个地址的哪个端口,为此我们要调用bind()函数,该函数调用成功返回0,否则返回SOCKET_ERROR:
- int bind(
- SOCKET s, //Socket对象名
- const struct sockaddr FAR *name, //Socket的地址值,即所在机器的IP地址
- int namelen //name的长度
- );
如果使用者不在意地址或端口的值,那么可以设定地址为INADDR_ANY,及Port为0,Windows Sockets会自动将其设定为适当的地址及Port(1024到5000之间的值)。此后可以调用getsockname()函数来获知其被设定的值。
(4) 监听
当服务器端的Socket对象绑定完成之后,必须建立一个监听的队列来接收客户端的连接请求。listen()函数使服务器端的Socket进入监听状态,并设定可以建立的最大连接数(目前最大值限制为5,最小值为1),该函数调用成功返回0,否则返回SOCKET_ERROR:
- int listen(
- SOCKET s, //需要建立监听的Socket
- int backlog //最大连接个数
- );
服务器端的Socket调用完listen()后,如果此时客户端调用connect()函数提出连接申请的话,服务器端必须再调用accept()函数,这样服务器端和客户端才算正式完成通信程序的连接动作。
为了知道什么时候客户端提出连接要求,从而服务器端的Socket在恰当的时候调用accept()函数完成连接的建立,我们就要使用WSAAsyncSelect()函数,让系统主动来通知我们有客户端提出连接请求了,该函数调用成功返回0,否则返回SOCKET_ERROR:
- int WSAAsyncSelect(
- SOCKET s, //Socket 对象
- HWND hWnd, //接收消息的窗口句柄
- unsigned int wMsg, //传给窗口的消息
- long lEvent //被注册的网络事件
- );
被注册的网络事件lEvent就是应用程序向窗口发送消息的网路事件,该值为下列值FD_READ、FD_WRITE、FD_OOB、FD_ACCEPT、FD_CONNECT、FD_CLOSE的组合,各个值的具体含义如下。
FD_READ:希望在套接字s收到数据时收到消息。
FD_WRITE:希望在套接字s上可以发送数据时收到消息。
FD_ACCEPT:希望在套接字s上收到连接请求时收到消息。
FD_CONNECT:希望在套接字s上连接成功时收到消息。
FD_CLOSE:希望在套接字s上连接关闭时收到消息。
FD_OOB:希望在套接字s上收到OOB数据时收到消息。
具体应用时,wMsg是在应用程序中定义的消息名称,而消息结构中的lParam则为以上各种网络事件名称。所以,可以在窗口处理自定义消息函数中使用以下结构来响应Socket的不同事件:
- switch(lParam) {
- case FD_READ:
- ...
- break;
- case FD_WRITE:
- ...
- break;
- ...
- }
(5) 服务器端接受客户端的连接请求
当Client提出连接请求时,Server端的hwnd窗口会收到Winsock Stack送来的我们自定义的一个消息,这时,我们可以分析lParam,然后调用相关的函数来处理此事件。为了使服务器端接受客户端的连接请求,就要使用accept()函数,该函数新建一个Socket与客户端的Socket相通,原先监听的Socket继续进入监听状态,等待其他客户端的连接要求,该函数调用成功返回一个新产生的Socket对象,否则返回INVALID_SOCKET:
- SOCKET accept(
- SOCKET s, //Socket的识别码
- struct sockaddr FAR *addr, //存放连接的客户端地址
- int FAR *addrlen //地址长度
- );
(6) 结束Socket连接
结束服务器和客户端的通信连接是很简单的,这一过程可以由服务器或客户机的任一端启动,只要调用closesocket()就可以了,而要关闭Server端监听状态的Socket,同样也是利用此函数。另外,与程序启动时调用WSAStartup()函数相对应,程序结束前,需要调用WSACleanup()来通知Winsock Stack释放Socket所占用的资源。这两个函数都是调用成功返回0,否则返回SOCKET_ERROR。closesocket()函数的原型如下:
- int closesocket(
- SOCKET s; //Socket的识别码
- );
(7) 最后调用WSACleanup
代码如下:
- int WSACleanup(void);
2.客户端编程步骤
(1) 建立客户端的Socket
客户端应用程序首先也是调用WSAStartup()函数来与Winsock的动态链接库建立关系,然后同样调用socket()来建立一个TCP或UDP Socket(相同协定的Sockets才能相通,TCP对TCP,UDP对UDP)。与服务器端的Socket不同的是,客户端的Socket可以调用 bind()函数,由自己来指定IP地址及port号码;但是也可以不调用bind(),而由Winsock来自动设定IP地址及port号码。
(2) 提出连接请求
客户端的Socket使用connect()函数来提出与服务器端的Socket建立连接的申请,函数调用成功返回0,否则返回SOCKET_ERROR:
- int connect(
- SOCKET s, //服务器端Socket的识别码
- const struct sockaddr FAR *name, //Socket想要连接的对方地址
- int namelen //地址长度
- );
作为客户端的监控程序,其实现过程要比服务器简单许多。由于需要接收数据,因此在异步选择函数中需要设定待监测的网络事件为FD_CLOSE和FD_READ。在消息响应函数中可以通过对消息参数的低位字节进行判断而区分出具体发生的是何种网络事件,并对其做出相应的反应。
3.数据的传送
基于TCP/IP连接协议(流式套接字)的服务是设计客户机/服务器应用程序时的主流标准,但有些服务是可以通过无连接协议(数据报套接字)提供的。一般情况下TCP Socket的数据发送和接收是调用send()及recv()这两个函数来达成,而UDP Socket则是用sendto()及recvfrom()这两个函数,这两个函数调用成功返回发送或接收的资料的长度,否则返回SOCKET_ERROR。send()函数的原型如下:
- int send(
- SOCKET s, //Socket的识别码
- const char FAR *buf, //存放要传送的资料的暂存区
- int len, //buf的长度
- int flags //此函数被调用的方式
- );
对于Datagram Socket而言,若是Datagram的大小超过限制,则将不会送出任何资料,并会传回错误值。对Stream Socket而言,在Blocking模式下,若是传送系统内的存储空间不够存放这些要传送的资料,send()将会被block住,直到资料送完为止;如果该Socket被设定为 Non-Blocking模式,那么将视目前的output buffer空间有多少,就送出多少资料,并不会被block住。
flags的值可设为0或MSG_DONTROUTE及MSG_OOB的组合。
recv()函数的原型如下:
- int recv(
- SOCKET s, // Socket的识别码
- char FAR *buf, // 存放接收到资料的暂存区
- int len, // buf的长度
- int flags; // 此函数被调用的方式
- );