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>
intselect(nfds,readfds,writefds,exceptfds,timeout)
intnfds;
fd_set*readfds,*writefds,*exceptfds;
structtimeval*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(intfd,fd_set*fdset)
:建立文件句柄
fd
与
fdset
的联系。
FD_CLR(intfd,fd_set*fdset)
:清除文件句柄
fd
与
fdset
的联系。
FD_ISSET(intfd,fdset*fdset)
:检查
fdset
联系的文件句柄
fd
是否可读写,
>0
表示可读写。
(关于
fd_set
及相关宏的定义见
/usr/include/sys/types.h
)
这样,你的
socket
只需在有东东读的时候才读入,大致如下:
...
intsockfd;
fd_setfdR;
structtimevaltimeout=..;
...
for(;;){
FD_ZERO(&fdR);
FD_SET(sockfd,&fdR);
switch(select(sockfd+1,&fdR,NULL,&timeout)){
case-1:
errorhandledbyu;
case0:
timeouthanledbyu;
default:
if(FD_ISSET(sockfd)){
nowureadorrecvsomething;
/*ifsockfdisfatherand
serversocket,ucannow
accept()*/
}
}
}
所以一个
FD_ISSET(sockfd)
就相当通知了
sockfd
可读。
至于
structtimeval
在此的功能,请
manselect
。不同的
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
的安装目录下。