TCP/IP协议实际上就是在物理网上的一组完整的网络协议。其中TCP是提供传输 层服务,而IP则是提供网络层服务。TCP/IP协议包括如下协议,其结构如图所示。
IP: 网间协议(Internet Protocol) 负责主机间数据的路由和网络上数据的存储。 同时为ICMP,TCP,UDP提供分组发送服务。用户进程通常不需要涉及这一层。
ARP: 地址解析协议(Address Resolution Protocol),此协议将网络地址映射 到硬件地址。
RARP:反向地址解析协议(Reverse Address Resolution Protocol),此协议 将硬件地址映射到网络地址。
ICMP:网间报文控制协议(Internet Control Message Protocol),此协议处理 信关和主机的差错和传送控制。
TCP:传送控制协议(Transmission Control Protocol),这是一种提供给用户 进程的可靠的全双工字节流面向连接的协议。它要为用户进程提供虚电路服务, 并为数据可靠传输建立检查。
UDP:用户数据报协议(User Datagram Protocol),这是提供给用户进程的无 连接协议,用于传送数据而不执行正确性检查。
FTP:文件传输协议(File Transfer Protocol),允许用户以文件操作的方式
(文件的增、删、改、查、传送等)与另一主机相互通信。
SMTP:简单邮件传送协议(Simple Mail Transfer Protocol),SMTP协议为系 统之间传送电子邮件。
TELNET:终端协议(Telnet Terminal Procotol),允许用户以虚终端方式访问 远程主机。
HTTP:超文本传输协议(Hypertext Transfer Procotol)。
TFTP:简单文件传输协议(Trivial File Transfer Protocol)。
TCP/IP协议的核心部分是传输层协议(TCP、UDP)、网络层协议(IP)和物理 接口层,这三层通常是在操作系统内核中实现,因此用户一般不涉及。编程时, 编程界面有两种形式:
第一种是由内核心直接提供的系统调用;
第二种是使用以库函数方式提供的各种函数。前者为核内实现,后者为核外实现。 用户服务要通过核外的应用程序才能实现,所以要使用套接字(socket)来实现。 TCP/IP协议核心与应用程序关系如图所示。
WinSock并不是一种网络协议,它只是一个网络编程接口,也就是说,它不是协议,但是它可以访问很多种网络协议,可以把它当作一些协议的封装。现在的
WinSock已经基本上实现了与协议无关。可以使用WinSock来调用多种协议的功能。 那么,WinSock和TCP/IP协议到底是什么关系呢?实际上,WinSock就是TCP/IP
协议的一种封装,可通过调用WinSock的接口函数来调用TCP/IP的各种功能。
在TCP/IP网络中两个进程间的相互作用的主机模式是客户机/服务器模式
(Client/Server model)。该模式的建立基于以下两点:
第一,非对等作用;
第二,通信完全是异步的。
客户机/服务器模式在操作过程中采取的是主动请示方式:
首先服务器方要先启动,并根据请示提供相应服务,过程如下:
为了更好说明套接字编程原理,这里介绍几个基本的套接字。
面向连接(基于TCP)socket 编程
为了实现服务器与客户机的通信,服务器和客户机都必须建立套接字。服务器 与客户机的工作原理可以用下面的过程来描述:
客户机与服务器建立面向连接的(基于TCP)套接字进行通信,请求与响应过程 及应用程序流程可用如图表示。
面向无连接(基于UDP)的socket 编程
无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与 服务程序之间的相互作用。
1.启动与终止
在所有Windows Sockets 函数中,只有启动函数WSAStartup()和终止函数 WSACleanup()是必须使用的。启动函数必须是第一个使用的函数,而且它允许 指定 Windows Sockets API 的版本,并获得 SOCKETS的特定的一些技术细节。 函数原型如下:
int PASCAL FAR WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
其中:wVersionRequested保证SOCKETS可正常运行的DLL版本,如果不支持, 则返回错误信息。
下面这段代码,说明如何进行WSAStartup()的调用。
WORD wVersionRequested; // 定义版本信息变量 WSADATA wsaData; //定义数据信息变量 int err; //定义错误号变量 wVersionRequested = MAKEWORD(1,1); //给版本信息赋值 err = WSAStartup(wVersionRequested, &wsaData); //给错误信息赋值 if(err!=0) { return; //通知用户找不到合适的版本 } //确认 Windows Sockets DLL 支持 1.1 版本 //DLL 版本可以高于 1.1 //系统返回的版本号始终是最低要求的 1.1,即应用程序与DLL 中可支持的最低版 本号 if(LOBYTE(wsaData.wVersion)!= 1|| HIBYTE(wsaData.wVersion)!=1) { WSACleanup(); //通知用户找不到合适的版本 return; } //Windows Sockets DLL 被进程接受,可以进入下一步操作
关闭函数使用时,任何打开并已连接的 SOCK_STREAM 套接字被复位,但那些 已由 closesocket()函数关闭的但仍有未发送数据的套接字不受影响,未发送的数 据仍将被发送。程序运行时可能会多次调用WSAStartuo()函数,但必须保证每次 调用时的 wVersionRequested 的值是相同的。
2.异步请求服务
Windows Sockets 除支持Berkeley Sockets中同步请求,还增加了一类异步请求 服务函数WSAAsyncGerXByY()。该函数是阻塞请求函数的异步版本。应用程序 调用它时,由 Windows Sockets DLL初始化这一操作并返回调用者,此函数返 回一个异步句柄,用来标识这个操作。当结果存储在调用者提供的缓冲区,并且 发送一个消息到应用程序相应窗口。
常用结构如下:
HANDLE taskHnd; char hostname="rs6000"; taskHnd = WSAAsyncBetHostByName(hWnd,wMsg,hostname,buf,buflen);
需要注意,由于Windows 的内存对像可以设置为可移动和可丢弃,因此在操作 内存对象时,必须保证WIindows Sockets DLL对象是可用的。
3.异步数据传输
使用send()或sendto()函数来发送数据,使用recv()或recvfrom()来接收数据。 Windows Sockets不鼓励用户使用阻塞方式传输数据,因为那样可能会阻塞整个 Windows 环境。下面看一个异步数据传输实例:
假设套接字s 在连接建立后,已经使用了函数 WSAAsyncSelect()在其上注册了 网络事件FD_READ和FD_WRITE,并且wMsg值为UM_SOCK,那么就可以在 Windows 消息循环中增加如下的分支语句:
case UM_SOCK: switch(lParam) { case FD_READ: len = recv(wParam,lpBuffer,length,0); break; case FD_WRITE: while(send(wParam,lpBuffer,len,0)!=SOCKET_ERROR) break; } break;
4.出错处理
Windows提供了一个函数来获取最近的错误码WSAGetLastError(),推荐的编写 方式如下:
len = send (s,lpBuffer,len,0); of((len==SOCKET_ERROR)&& (WSAGetLastError()==WSAWOULDBLOCK)) { ... }
WinSock编程分为服务器端和客户端两部分,TCP服务器端的大体流程如下:
int WSAStarup( WORD wVersionRequested,LPWSADATA lpWsAData )
初始化WinSock 。参数wVersionRequested是要求使用的WinSock的版本。
SOCKET Socket(int af,int type,int protocol)。
来实现。套接字可以说是WinSock通讯的核心。WinSock通讯的所有数据传输, 都是通过套接字来完成的,套接字包含了两个信息,一个是IP地址,一个是Port 端口号,使用这两个信息,就可以确定网络中的任何一个通讯节点。
int bind(SOCKET s,const struct sockaddr FAR* name,int namelen); struct sockaddr_in { short sin_family ; u_short sin_prot ; struct in_addr sin_addr ; char sin_sero[8] ; }
这就包含了需要建立连接的本地的地址,包括地址族、IP和端口信息。 sin_family字段必须把它设为AF_INET,这是告诉WinSock使用的是IP地址族。 sin_prot就是要用来通讯的端口号。sin_addr就是要用来通讯的IP地址信息。
由于各种不同的计算机处理数据时的方法是不一样的,Intel X86处理器表示多字 节的编号时,把低字节放在前面,把高字节放在后面,而互联网标准却正好相反, 所以,必须把主机字节转换成网络字节的顺序。
把主机字节转化成网络字节的函数:
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
把网络字节转化成主机字节的函数:
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort) ;
这样,设置IP地址和port端口时,就必须把主机字节转化成网络字节后,才能用 Bind()函数来绑定套接字和地址。当绑定完成之后,服务器端必须建立一个监听 的队列来接收客户端的连接请求。
int listen(SOCKET s,int backlog);
这个函数可以把套接字转成监听模式。
如果客户端有了连接请求,还必须使用:
int accept(SOCKET s,struct sockaddr FAR* addr,int FAR* addrlen);
来接受客户端的请求。
现在基本上已经完成了一个服务器的建立,而客户端的建立的流程则是初始化WinSock,然后创建Socket套接字,再使用:
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen);
来连接服务端。
WSADATA wsd; SOCKET sListen; SOCKET sclient; UINT port = 800; int iAddrSize; struct sockaddr_in local , client; WSAStartup(0x11 , &wsd); sListen = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP); local.sin_family = AF_INET; local.sin_addr = htonl(INADDR_ANY ); local.sin_port = htons( port ); bind( sListen , (struct sockaddr*)&local , sizeof( local )); listen(sListen , 5); sClient = accept(sListen , (struct sockaddr*)&client ,&iAddrSize);客户端的创建:
WSADATA wsd; SOCKET sClient; UINT port = 800;// 此端口根据服务器提供 char szIp[] = "127.0.0.1"; int iAddrSize; struct sockaddr_in server; WSAStartup( 0x11 , &wsd); sClient = Socket( AF_INET , SOCK_STREAM , IPPOTO_IP); server.sin_family = AF_INET; server.sin_addr = inet_addr( szIp ); server.sin_port = htons( port ); connect(sClient , (struct sockaddr*)&server , sizeof( server));
int send(SOCKET s,const char FAR* buf,int len,int flags); int recv( SOCKET s,char FAR* buf,int len,int flags);
int shutdown(SOCKET s,int how);
int closeSocket(SOCKET s);