1.Soket发展史以及它和tcp/ip的关系
七十年代中,美国国防部高研署(DARPA)将TCP/IP的软件提供给加利福尼亚大学Berkeley分校后,TCP/IP很快被集成到 Unix中,同时出现了许多成熟的TCP/IP应用程序接口(API)。这个API称为Socket接口。今天,SOCKET接口是TCP/IP网络最为 通用的API,也是在INTERNET上进行应用开发最为通用的API。
九十年代初,由Microsoft联合了其他几家公司共同制定了一套 WINDOWS下的网络编程接口,即Windows Sockets规范。它是Berkeley Sockets的重要扩充,主要是增加了一些异步函数,并增加了符合 Windows 消息驱动特性的网络事件异步选择机制。 Windows Sockets规范是一套开放的、支持多种协议的 Windows下的网络编程接口。目前,在实际应用中的Windows Sockets规范主要有1.1版和2.0版。两者的最重要区别是1.1版只支持TCP/IP协议,而2.0版可以支持多协议,2.0版有良好的向后兼容 性,目前,Windows下的Internet软件都是基于 WinSock开发的。
Socket实际在计算机中提供了一个通信端口,可以通过这个端口与任何一个具有Socket接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个Socket接口来实现。在应用开发中就像使用文件 句柄一样,可以对 Socket句柄进行读、写操作。套接字是网络的基本构件。它是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。套接字存在通信区域(通信区域又称地址簇)中。套接字只与同一区域中的套接字交换数据(跨区域时,需要执行某和转换进程才能实现)。WINDOWS 中的套接字只支持一个域——网际域。套接字具有类型。我们将 Socket翻译为套接字,套接字分为以下三种类型:
字节流套接字(Stream Socket) 是最常用的套接字类型,TCP/IP协议族中的 TCP 协议使用此类接口。字节流套接口提供面向连接的(建立虚电路)、无差错的、发送先后顺序一致的、无记录边界和非重复的网络信包传输。
数据报套接字 (Datagram Socket) TCP/IP协议族中的UDP协议使用此类接口,它是无连接的服务,它以独立的信包进行网络传输,信包最大长度为32KB,传输不保证顺 序性、可靠性和无重复性,它通常用于单个报文传输或可靠性不重要的场合。数据报套接口的一个重要特点是它保留了记录边界。对于这一特点。数据报套接口采用了与现在许多包交换网络(例如以太网)非常类似的模型。
原始数据报套接字(Raw Socket) 提供对网络下层通讯协议(如IP协议)的直接访问,它一般不是提供给普通用户的,主要用于开发新的协议或用于提取协议较隐蔽的功能。
2、socket通信概念
端口
网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。
按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。 从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此,TCP/IP协议提出了协议端口(protocol port,简称端口)的概念,用于标识通信的进程。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序(即进程)通过系统调 用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。
类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。由于TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立,如TCP有一个255号端口,UDP也可以有一个255号端口,二者并不冲突。
地址
网络通信中通信的两个进程分别在不同的机器上。在互连网络中,两台机器可能位于不同的网络,这些网络通过网络互连设备(网关,网桥,路由器等)连接。因此需要三级寻址:
某一主机可与多个网络相连,必须指定一特定网络地址;
网络上每一台主机应有其唯一的地址;
每一主机上的每一进程应有在该主机上的唯一标识符。
通常主机地址由网络ID和主机ID组成,在TCP/IP协议中用32位整数值表示;TCP和UDP均使用16位端口号标识用户进程。
网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低价先存),有的存高位字节(高价先存)。为保证数据的正确性,在网络协议中须指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高价先存格式,它们均含在协议头文件中。
面向连接
可靠的报文流、可靠的字节流、可靠的连接,如:文件传输(FTP)、远程登录(Telnet)
数字话音。
无连接
不可靠的数据报、有确认的数据报、请求-应答,如:电子邮件(E-mail)、电子邮件中的挂号信、网络数据库查询。
顺序
在网络传输中,两个连续报文在端-端通信中可能经过不同路径,这样到达目的地时的顺序
可能会与发送时不同。"顺序"是指接收数据顺序与发送数据顺序相同。TCP协议提供这项服务。
差错控制
保证应用程序接收的数据无差错的一种机制。检查差错的方法一般是采用检验、检查和Checksum的方法。而保证传送无差错的方法是双方采用确认应答技术。TCP协议提供这项服务。
流控制
在数据传输过程中控制数据传输速率的一种机制,以保证数据不被丢失。TCP协议提供这项服务。
字节流
字节流方式指的是仅把传输中的报文看作是一个字节序列,不提供数据流的任何边界。TCP协议提供字节流服务。
报文
接收方要保存发送方的报文边界。UDP协议提供报文服务。
全双工/半双工
端-端间数据同时以两个方向/一个方向传送。
缓存/带外数据
在字节流服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。为保证传输正确或采用有流控制的协议时,都要进行缓存。但对某些特殊的需求,如交互式应用程序,又会要求取消这种缓存。
客户/服务器模式
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式(Client/Server model),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务器模式的TCP/IP。
客户/服务器模式在操作过程中采取的是主动请求方式:
首先服务器方要先启动,并根据请求提供相应服务:
打开一通信通道并告知本地主机,它愿意在某一公认地址上(周知口,如FTP为 21)接收客户请求;
等待客户请求到达该端口;
接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
返回第二步,等待另一客户请求。
关闭服务器
客户方:
打开一通信通道,并连接到服务器所在主机的特定端口;
向服务器发服务请求报文,等待并接收应答;继续提出请求......
请求结束后关闭通信通道并终止。
从上面所描述过程可知:
客户与服务器进程的作用是非对称的,因此编码不同。
服务进程一般是先于客户请求而启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。
3.socket 通信五元组
SOCKET PASCAL FAR socket(int af, int type, int protocol)
该调用要接收三个参数:af、type、protocol。参数af指定通信发生的区域,UNIX系统支持的地址族有:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。参数type 描述要建立的套接字的类型。参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的"协议"这一元。
TCP/IP的socket提供下列三种类型套接字。
流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
数据报式套接字(SOCK_DGRAM)
提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
原始式套接字(SOCK_RAW)
该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen)
参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。
调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。
监听连接──listen()
此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。
recv()调用用于在参数s指定的已连接的数据报或流套接字上接收输入数据,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
参数s 为已连接的套接字描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。
send()调用用于在参数s指定的已连接的数据报或流套接字上发送输出数据,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。
输入/输出多路复用──select()
select()调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个fd_set结构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select()调用返回满足条件的套接字的数目,其调用格式如下:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
参数nfds指明被检查的套接字描述符的值域,此变量一般被忽略。
参数readfds指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。参数writefds 指向要做写检测的套接字描述符集合的指针。exceptfds指向要检测是否出错的套接字描述符集合的指针。timeout指向select()函数等待的最大时间,如果设为NULL则为阻塞操作。select()返回包含在fd_set结构中已准备好的套接字描述符的总数目,或者是发生错误则返回SOCKET_ERROR。
select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读,下面具体解释:
#include <sys/types.h>
#include <sys/times.h>
#include <sys/select.h>
int select(nfds, readfds, writefds, exceptfds, timeout)
int nfds;
fd_set *readfds, *writefds, *exceptfds;
struct timeval *timeout;
ndfs:select监视的文件句柄数,视进程中打开的文件数而定,一般设为呢要监视各文件中的最大文件号加一。
readfds:select监视的可读文件句柄集合。
writefds: select监视的可写文件句柄集合。
exceptfds:select监视的异常文件句柄集合。
timeout:本次select()的超时结束时间。(见/usr/sys/select.h,可精确至百万分之一秒!)
当readfds或writefds中映象的文件可读或可写或超时,本次select()
就结束返回。程序员利用一组系统提供的宏在select()结束时便可判
断哪一文件可读或可写。对Socket编程特别有用的就是readfds。
几只相关的宏解释如下:
FD_ZERO(fd_set *fdset):清空fdset与所有文件句柄的联系。
FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。
FD_ISSET(int fd, fdset *fdset):检查fdset联系的文件句柄fd是否可读写,>0表示可读写。
(关于fd_set及相关宏的定义见/usr/include/sys/types.h)
这样,你的socket只需在有东东读的时候才读入,大致如下:
...
int sockfd;
fd_set fdR;
struct timeval timeout = ..;
...
for(;;) {
FD_ZERO(&fdR);
FD_SET(sockfd, &fdR);
switch (select(sockfd + 1, &fdR, NULL, &timeout)) {
case -1:
error handled by u;
case 0:
timeout hanled by u;
default:
if (FD_ISSET(sockfd)) {
now u read or recv something;
/* if sockfd is father and
server socket, u can now
accept() */
}
}
}
所以一个FD_ISSET(sockfd)就相当通知了sockfd可读。 至于struct timeval在此的功能,请man select。不同的timeval设置使使select()表现出超时结束、无超时阻塞和轮询三种特性。由于 timeval可精确至百万分之一秒,所以Windows的SetTimer()根本不算什么。你可以用select()做一个超级时钟。
服务器方程序:
/* File Name: streams.c */
#include
#include
#define TRUE 1
/* 这个程序建立一个套接字,然后开始无限循环;每当它通过循环接收到一个连接,则打印出一个信息。当连接断开,或接收到终止信息,则此连接结束,程序再接收一个新的连接。命令行的格式是:streams */
main( )
{
int sock, length;
struct sockaddr_in server;
struct sockaddr tcpaddr;
int msgsock;
char buf[1024];
int rval, len;
/* 建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("opening stream socket");
exit(1);
}
/* 使用任意端口命名套接字 */
server.sin_family = AF_INET;
server.sin_port = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("binding stream socket");
exit(1);
}
/* 找出指定的端口号并打印出来 */
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
perror("getting socket name");
exit(1);
}
printf("socket port #%d/n", ntohs(server.sin_port));
/* 开始接收连接 */
listen(sock, 5);
len = sizeof(struct sockaddr);
do {
msgsock = accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
if (msgsock == -1)
perror("accept");
else do{
memset(buf, 0, sizeof(buf));
if ((rval = recv(msgsock, buf, 1024)) < 0)
perror("reading stream message");
if (rval == 0)
printf("ending connection /n");
else
printf("-->%s/n", buf);
}while (rval != 0);
closesocket(msgsock);
} while (TRUE);
/* 因为这个程序已经有了一个无限循环,所以套接字"sock"从来不显式关闭。然而,当进程被杀死或正常终止时,所有套接字都将自动地被关闭。*/
exit(0);
}
客户方程序:
/* File Name: streamc.c */
#include
#include
#define DATA "half a league, half a league ..."
/* 这个程序建立套接字,然后与命令行给出的套接字连接;连接结束时,在连接上发送
一个消息,然后关闭套接字。命令行的格式是:streamc 主机名 端口号
端口号要与服务器程序的端口号相同 */
main(argc, argv)
int argc;
char *argv[ ];
{
int sock;
struct sockaddr_in server;
struct hostent *hp, *gethostbyname( );
char buf[1024];
/* 建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("opening stream socket");
exit(1);
}
/* 使用命令行中指定的名字连接套接字 */
server.sin_family = AF_INET;
hp = gethostbyname(argv[1]);
if (hp == 0) {
fprintf(stderr, "%s: unknown host /n", argv[1]);
exit(2);
}
memcpy((char*)&server.sin_addr, (char*)hp->h_addr, hp->h_length);
sever.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
perror("connecting stream socket");
exit(3);
}
if (send(sock, DATA, sizeof(DATA)) < 0)
perror("sending on stream socket");
closesocket(sock);
exit(0);
}
4.Windows socket 程序设计
Windows Sockets 是从 Berkeley Sockets 扩展而来的,其在继承 Berkeley Sockets 的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合WINDOWS消息驱动特性的网络事件异步选择机制。
Windows Sockets由两部分组成:开发组件和运行组件。
开发组件:Windows Sockets 实现文档、应用程序接口(API)引入库和一些头文件。
运行组件:Windows Sockets 应用程序接口的动态链接库(WINSOCK.DLL)。
在Microsoft Windows下开发Windows Sockets网络程序与在UNIX环境下开发Berkeley Sockets网络程序有一定的差别,这主要时因为Windows是非抢先多任务环境,各任务之间的切换是通过消息驱动的。因此,在Windows下开发Sockets网络程序要尽量避开阻塞工作方式,而使用Windows Sockets提供的基于消息机制的网络事件异步存取接口。
Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便地处理网络通信,它对网络事件采用了基于消息的异步存取策略。基于这一策略,Windows Sockets在如下方面作了扩充:
异步选择机制
UNIX下Sockets对于异步事件的选择是靠调用select()函数来查询的,这种方式对于Windows应用程序来说是难以接受的。Windows Sockets的异步选择函数提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。
异步请求函数
在标准Berkeley Sockets中,请求服务是阻塞的。Windows Sockets除了支持这一类函数外,还增加了相应的异步请求服务函数WSAASyncGetXByY()。这些异步请求函数允许应用程序采用异步方式获取请求信息,并且在请求的服务完成时给应用程序相应的窗口函数发送一个消息。
阻塞处理方法
Windows Sockets为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃CPU让其它应用程序运行,它在调用处于阻塞时便进入一个叫“HOOK”的例程,此例程负责接收和分配Windows消息,这使得其它应用程序仍然能够接收到自己的消息并取得控制权。Windows Sockets还提供了两个函数(WSASetBlockingHook()和WSAUnhookBlockingHook())让用户设置和取消自己的阻塞处理例程,以支持要求复杂消息处理的应用程序(如多文档界面)。
出错处理
Windows Sockets为了和以后多线程环境(如Windows/NT)兼容,它提供了两个出错处理函数WSAGetLastError()和WSASetLastError()来获取和设置当前线程的最近错误号,而不使用Berkeley Sockets中的全局变量errno和h_errno。
启动与终止
对于所有在Windows Sockets上开发的应用程序,在它使用任何Windows Sockets API调用之前,必须先调用启动函数WSAStartup(),它完成Windows Sockets DLL的初始化;协商版本支持,分配必要的资源。在应用程序完成了对Windows Sockets的使用之后,它必须调用函数WSACleanup()来从Windows Sockets实现中注销自己,并允许实现释放为其分配的任何资源。
服务器端操作 socket(套接字)
在初始化阶段调用WSAStartup()
此函数在应用程序中初始化Windows Sockets DLL ,只有此函数调用成功后,应用程序才可以再调用其他Windows Sockets DLL中的API函数。在程式中调用该函数的形式如下:WSAStartup((WORD)((1<<8|1),(LPWSADATA) &WSAData),其中(1<<8|1)表示我们用的是WinSocket1.1版本,WSAata用来存储系统传回的关于 WinSocket的资料。
建立Socket
初始化WinSock的动态连接库后,需要在服务器端建立一个 监听的Socket,为此可以调用Socket()函数用来建立这个监听的Socket,并定义此Socket所使用的通信协议。此函数调用成功返回 Socket对象,失败则返回INVALID_SOCKET(调用WSAGetLastError()可得知原因,所有WinSocket 的函数都可以使用这个函数来获取失败的原因)。
SOCKET PASCAL FAR socket( int af, int type, int protocol )
参数: af:目前只提供 PF_INET(AF_INET);
type:Socket 的类型 (SOCK_STREAM、SOCK_DGRAM);
protocol:通讯协定(如果使用者不指定则设为0);
如果要建立的是遵从TCP/IP协议的socket,第二个参数type应为SOCK_STREAM,如为UDP(数据报)的socket,应为SOCK_DGRAM。
绑定端口
接下来要为服务器端定义的这个监听的Socket指定一个地址及端口(Port),这样客户端才知道待会要连接哪一个地址的哪个端口,为此我们要调用bind()函数,该函数调用成功返回0,否则返回SOCKET_ERROR。
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );
参 数: s:Socket对象名;
name:Socket的地址值,这个地址必须是执行这个程式所在机器的IP地址;
namelen:name的长度;
如果使用者不在意地址或端口的值,那么可以设定地址为INADDR_ANY,及Port为0,Windows Sockets 会自动将其设定适当之地址及Port (1024 到 5000之间的值)。此后可以调用getsockname()函数来获知其被设定的值。
监听
当服务器端的Socket对象绑定完成之后,服务器端必须建立一个监听的队列来接收客户端的连接请求。listen()函数使服务器端的Socket 进入监听状态,并设定可以建立的最大连接数(目前最大值限制为 5, 最小值为1)。该函数调用成功返回0,否则返回SOCKET_ERROR。
int PASCAL FAR listen( SOCKET s, int backlog );
参 数: s:需要建立监听的Socket;
backlog:最大连接个数;
异步选择
服务器端的Socket调用完listen()后,如果此时客户端调用connect()函数提出连接申请的话,Server 端必须再调用accept() 函数,这样服务器端和客户端才算正式完成通信程序的连接动作。为了知道什么时候客户端提出连接要求,从而服务器端的Socket在恰当的时候调用 accept()函数完成连接的建立,我们就要使用WSAAsyncSelect()函数,让系统主动来通知我们有客户端提出连接请求了。该函数调用成功 返回0,否则返回SOCKET_ERROR。
int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent );
参数: s:Socket 对象;
hWnd :接收消息的窗口句柄;
wMsg:传给窗口的消息;
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上收到带外数据时收到消息。
具体应用时,wMsg应是在应用程序中定义的消息名称,而消息结构中的lParam则为以上各种网络事件名称。所以,可以在窗口处理自定义消息函数中使用以下结构来响应Socket的不同事件:
switch(lParam)
{
case FD_READ:
…
break;
case FD_WRITE、
…
break;
…
}
服务器端接受客户端的连接请求
当Client提出连接请求时,Server 端hwnd视窗会收到Winsock Stack送来我们自定义的一 个消息,这时,我们可以分析lParam,然后调用相关的函数来处理此事件。为了使服务器端接受客户端的连接请求,就要使用 accept() 函数,该函数新建一Socket与客户端的Socket相通,原先监听之Socket继续进入监听状态,等待他人的连接要求。该函数调用成功返回一个新产生的Socket对象,否则返回INVALID_SOCKET。
SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen );
参数:s:Socket的识别码;
addr:存放来连接的客户端的地址;
addrlen:addr的长度
结束 socket 连接
结束服务器和客户端的通信连接是很简单的,这一过程可以由服务器或客户机的任一端启动,只要调用closesocket()就可以了,而要关闭 Server端监听状态的socket,同样也是利用此函数。另外,与程序启动时调用WSAStartup()憨数相对应,程式结束前,需要调用 WSACleanup() 来通知Winsock Stack释放Socket所占用的资源。这两个函数都是调用成功返回0,否则返回SOCKET_ERROR。
int PASCAL FAR closesocket( SOCKET s );
参 数:s:Socket 的识别码;
int PASCAL FAR WSACleanup( void );
参 数: 无
客户端Socket的操作
建立客户端的Socket
客户端应用程序首先也是调用WSAStartup() 函数来与Winsock的动态连接库建立关系,然后同样调用socket() 来建立一个TCP或UDP socket(相同协定的 sockets 才能相通,TCP 对 TCP,UDP 对 UDP)。与服务器端的socket 不同的是,客户端的socket 可以调用 bind() 函数,由自己来指定IP地址及port号码;但是也可以不调用 bind(),而由 Winsock来自动设定IP地址及port号码。
提出连接申请
客户端的Socket使用connect()函数来提出与服务器端的Socket建立连接的申请,函数调用成功返回0,否则返回SOCKET_ERROR。
int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
参 数:s:Socket 的识别码;
name:Socket想要连接的对方地址;
namelen:name的长度
数据的传送
虽然基于TCP/IP连接协议(流套接字)的服务是设计客户机/服务器应用程序时的主流标准,但有些服务也是可以通过无连接协议(数据报套接字)提供的。先介绍一下TCP socket 与UDP socket 在传送数据时的特性:Stream (TCP) Socket 提供双向、可靠、有次序、不重复的资料传送。Datagram (UDP) Socket 虽然提供双向的通信,但没有可靠、有次序、不重复的保证,所以UDP传送数据可能会收到无次序、重复的资料,甚至资料在传输过程中出现遗漏。由于UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以绝大多数应用程序都是采用TCP处理Socket,以保证资料的正确性。一般情况下TCP Socket 的数据发送和接收是调用send() 及recv() 这两个函数来达成,而 UDP Socket则是用sendto() 及recvfrom() 这两个函数,这两个函数调用成功发挥发送或接收的资料的长度,否则返回SOCKET_ERROR。
int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags );
参数:s:Socket 的识别码
buf:存放要传送的资料的暂存区
len buf:的长度
flags:此函数被调用的方式
对于Datagram Socket而言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对Stream Socket 言,Blocking 模式下,若是传送系统内的储存空间不够存放这些要传送的资料,send()将会被block住,直到资料送完为止;如果该Socket被设定为 Non-Blocking 模式,那么将视目前的output buffer空间有多少,就送出多少资料,并不会被 block 住。flags 的值可设为 0 或 MSG_DONTROUTE及 MSG_OOB 的组合。
int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );
参数:s:Socket 的识别码
buf:存放接收到的资料的暂存区
len buf:的长度
flags:此函数被调用的方式
对Stream Socket 言,我们可以接收到目前input buffer内有效的资料,但其数量不超过len的大小。
自定义的CMySocket类的实现代码:
根据上面的知识,自定义了一个简单的CMySocket类,下面是定义的该类的部分实现代码:
CMySocket::CMySocket() : file://类的构造函数
{
WSADATA wsaD;
memset( m_LastError, 0, ERR_MAXLENGTH );
// m_LastError是类内字符串变量,初始化用来存放最后错误说明的字符串;
// 初始化类内sockaddr_in结构变量,前者存放客户端地址,后者对应于服务器端地址;
memset( &m_sockaddr, 0, sizeof( m_sockaddr ) );
memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
int result = WSAStartup((WORD)((1<<8|1), &wsaD);//初始化WinSocket动态连接库;
if( result != 0 ) // 初始化失败;
{ set_LastError( "WSAStartup failed!", WSAGetLastError() );
return;
}
}
//////////////////////////////
CMySocket::~CMySocket() { WSACleanup(); }//类的析构函数;
////////////////////////////////////////////////////
int CMySocket::Create( void )
{// m_hSocket是类内Socket对象,创建一个基于TCP/IP的Socket变量,并将值赋给该变量;
if ( (m_hSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )) == INVALID_SOCKET )
{
set_LastError( "socket() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
///////////////////////////////////////////////
int CMySocket::Close( void )//关闭Socket对象;
{
if ( closesocket( m_hSocket ) == SOCKET_ERROR )
{
set_LastError( "closesocket() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
file://重置sockaddr_in 结构变量;
memset( &m_sockaddr, 0, sizeof( sockaddr_in ) );
memset( &m_rsockaddr, 0, sizeof( sockaddr_in ) );
return ERR_SUCCESS;
}
/////////////////////////////////////////
int CMySocket::Connect( char* strRemote, unsigned int iPort )//定义连接函数;
{
if( strlen( strRemote ) == 0 || iPort == 0 )
return ERR_BADPARAM;
hostent *hostEnt = NULL;
long lIPAddress = 0;
hostEnt = gethostbyname( strRemote );//根据计算机名得到该计算机的相关内容;
if( hostEnt != NULL )
{
lIPAddress = ((in_addr*)hostEnt->h_addr)->s_addr;
m_sockaddr.sin_addr.s_addr = lIPAddress;
}
else
{
m_sockaddr.sin_addr.s_addr = inet_addr( strRemote );
}
m_sockaddr.sin_family = AF_INET;
m_sockaddr.sin_port = htons( iPort );
if( connect( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
{
set_LastError( "connect() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
///////////////////////////////////////////////////////
int CMySocket::Bind( char* strIP, unsigned int iPort )//绑定函数;
{
if( strlen( strIP ) == 0 || iPort == 0 )
return ERR_BADPARAM;
memset( &m_sockaddr,0, sizeof( m_sockaddr ) );
m_sockaddr.sin_family = AF_INET;
m_sockaddr.sin_addr.s_addr = inet_addr( strIP );
m_sockaddr.sin_port = htons( iPort );
if ( bind( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
{
set_LastError( "bind() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
//////////////////////////////////////////
int CMySocket::Accept( SOCKET s )//建立连接函数,S为监听Socket对象名;
{
int Len = sizeof( m_rsockaddr );
memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
if( ( m_hSocket = accept( s, (SOCKADDR*)&m_rsockaddr, &Len ) ) == INVALID_SOCKET )
{
set_LastError( "accept() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::asyncSelect( HWND hWnd, unsigned int wMsg, long lEvent )
file://事件选择函数;
{
if( !IsWindow( hWnd ) || wMsg == 0 || lEvent == 0 )
return ERR_BADPARAM;
if( WSAAsyncSelect( m_hSocket, hWnd, wMsg, lEvent ) == SOCKET_ERROR )
{
set_LastError( "WSAAsyncSelect() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Listen( int iQueuedConnections )//监听函数;
{
if( iQueuedConnections == 0 )
return ERR_BADPARAM;
if( listen( m_hSocket, iQueuedConnections ) == SOCKET_ERROR )
{
set_LastError( "listen() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Send( char* strData, int iLen )//数据发送函数;
{
if( strData == NULL || iLen == 0 )
return ERR_BADPARAM;
if( send( m_hSocket, strData, iLen, 0 ) == SOCKET_ERROR )
{
set_LastError( "send() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::Receive( char* strData, int iLen )//数据接收函数;
{
if( strData == NULL )
return ERR_BADPARAM;
int len = 0;
int ret = 0;
ret = recv( m_hSocket, strData, iLen, 0 );
if ( ret == SOCKET_ERROR )
{
set_LastError( "recv() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ret;
}
void CMySocket::set_LastError( char* newError, int errNum )
file://WinSock API操作错误字符串设置函数;
{
memset( m_LastError, 0, ERR_MAXLENGTH );
memcpy( m_LastError, newError, strlen( newError ) );
m_LastError[strlen(newError)+1] = '/0';
}
有了上述类的定义,就可以在网络程序的服务器和客户端分别定义CMySocket对象,建立连接,传送数据了。例如,为了在服务器和客户端发送数据,需要在服务器端定义两个CMySocket对象ServerSocket1和ServerSocket2,分别用于监听和连接,客户端定义一个 CMySocket对象ClientSocket,用于发送或接收数据,如果建立的连接数大于一,可以在服务器端再定义CMySocket对象,但要注意 连接数不要大于五。
在VC中进行WINSOCK的API编程开发的时候,需要在项目中使用下面三个文件,否则会出现编译错误。
1.WINSOCK.H: 这是WINSOCK API的头文件,需要包含在项目中。
2.WSOCK32.LIB: WINSOCK API连接库文件。在使用中,一定要把它作为项目的非缺省的连接库包含到项目文件中去。
3.WINSOCK.DLL: WINSOCK的动态连接库,位于WINDOWS的安装目录下。