WinSock API 总结
我的
Windows Socket API
使用经验
本文是我在进行
MS-Windows
、
HP-Unix网络
编程的实践过程中总结出来的一些经验,仅供大家参考。本文所谈到的
Socket
函数如果没有特别说明,都是指的
Windows Socket API
。
一、
WSAStartup
函数
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData );
使用
Socket
的程序在使用
Socket
之前必须调用
WSAStartup
函数。该函数的第一个参数指明程序请求使用的
Socket
版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的
Socket
的版本信息。当一个应用程序调用
WSAStartup
函数时,操作系统根据请求的
Socket
版本来搜索相应的
Socket
库,然后绑定找到的
Socket
库到该应用程序中。以后应用程序就可以调用所请求的
Socket
库中的其它
Socket
函数了。该函数执行成功后返回
0
。
例:假如一个程序要使用
2.1
版本的
Socket,
那么程序代码如下
wVersionRequested = MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
二、
WSACleanup
函数
int WSACleanup (void);
应用程序在完成对请求的
Socket
库的使用后,要调用
WSACleanup
函数来解除与
Socket
库的绑定并且释放
Socket
库所占用的系统资源。
三、
socket
函数
SOCKET socket(int af, int type, int protocol );
应用程序调用
socket
函数来创建一个能够进行
网络
通信的套接字。第一个参数指定应用程序使用的通信协议的协议族,对于
TCP/IP
协议族,该参数置
PF_INET;
第二个参数指定要创建的套接字类型,流套接字类型为
SOCK_STREAM
、数据报套接字类型为
SOCK_DGRAM
;第三个参数指定应用程序所使用的通信协议。该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回
INVALID_SOCKET
。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。下面是一个创建流套接字的例子:
struct protoent *ppe;
ppe=getprotobyname("tcp");
SOCKET ListenSocket = socket(PF_INET,SOCK_STREAM,ppe->p_proto);
四、
closesocket
函数
int closesocket(SOCKET s );
closesocket
函数用来关闭一个描述符为
s
套接字。由于每个进程中都有一个套接字描述符表,表中的每个套接字描述符都对应了一个位于操作系统缓冲区中的套接字数据结构,因此有可能有几个套接字描述符指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构的被引用次数,即有多少个套接字描述符指向该结构。当调用
closesocket
函数时,操作系统先检查套接字数据结构中的该字段的值,如果为
1
,就表明只有一个套接字描述符指向它,因此操作系统就先把
s
在套接字描述符表中对应的那条表项清除,并且释放
s
对应的套接字数据结构;如果该字段大于
1
,那么操作系统仅仅清除
s
在套接字描述符表中的对应表项,并且把
s
对应的套接字数据结构的引用次数减
1
。
closesocket
函数如果执行成功就返回
0
,否则返回
SOCKET_ERROR
。
五、
send
函数
int send(SOCKET s, const char FAR *buf, int len, int flags);
不论是客户还是服务器应用程序都用
send
函数来向
TCP
连接的另一端发送数据。客户程序一般用
send
函数向服务器发送请求,而服务器则通常用
send
函数来向客户程序发送应答。该函数的第一个参数指定发送端套接字描述符;第二个参数指明一个存放应用程序要发送数据的缓冲区;第三个参数指明实际要发送的数据的字节数;第四个参数一般置
0
。这里只描述同步
Socket
的
send
函数的执行流程。当调用该函数时,
send
先比较待发送数据的长度
len
和套接字
s
的发送缓冲区的长度,如果
len
大于
s
的发送缓冲区的长度,该函数返回
SOCKET_ERROR
;如果
len
小于或者等于
s
的发送缓冲区的长度,那么
send
先检查协议是否正在发送
s
的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送
s
的发送缓冲中的数据或者
s
的发送缓冲中没有数据,那么
send
就比较
s
的发送缓冲区的剩余空间和
len
,如果
len
大于剩余空间大小
send
就一直等待协议把
s
的发送缓冲中的数据发送完,如果
len
小于剩余空间大小
send
就仅仅把
buf
中的数据
copy
到剩余空间里(注意并不是
send
把
s
的发送缓冲中的数据传到连接的另一端的,而是协议传的,
send
仅仅是把
buf
中的数据
copy
到
s
的发送缓冲区的剩余空间里)。如果
send
函数
copy
数据成功,就返回实际
copy
的字节数,如果
send
在
copy
数据时出现错误,那么
send
就返回
SOCKET_ERROR
;如果
send
在等待协议传送数据时
网络
断开的话,那么
send
函数也返回
SOCKET_ERROR
。要注意
send
函数把
buf
中的数据成功
copy
到
s
的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现
网络
错误的话,那么下一个
Socket
函数就会返回
SOCKET_ERROR
。(每一个除
send
外的
Socket
函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现
网络
错误,那么该
Socket
函数就返回
SOCKET_ERROR
)
注意:在
Unix
系统下,如果
send
在等待协议传送数据时
网络
断开的话,调用
send
的进程会接收到一个
SIGPIPE
信号,进程对该信号的默认处理是进程终止。
六、
recv
函数
int recv(SOCKET s, char FAR *buf, int len, int flags );
不论是客户还是服务器应用程序都用
recv
函数从
TCP
连接的另一端接收数据。该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放
recv
函数接收到的数据;第三个参数指明
buf
的长度;第四个参数一般置
0
。这里只描述同步
Socket
的
recv
函数的执行流程。当应用程序调用
recv
函数时,
recv
先等待
s
的发送缓冲中的数据被协议传送完毕,如果协议在传送
s
的发送缓冲中的数据时出现
网络
错误,那么
recv
函数返回
SOCKET_ERROR
,如果
s
的发送缓冲中没有数据或者数据被协议成功发送完毕后,
recv
先检查套接字
s
的接收缓冲区,如果
s
接收缓冲区中没有数据或者协议正在接收数据,那么
recv
就一直等待,只到协议把数据接收完毕。当协议把数据接收完毕,
recv
函数就把
s
的接收缓冲中的数据
copy
到
buf
中(注意协议接收到的数据可能大于
buf
的长度,所以在这种情况下要调用几次
recv
函数才能把
s
的接收缓冲中的数据
copy
完。
recv
函数仅仅是
copy
数据,真正的接收数据是协议来完成的),
recv
函数返回其实际
copy
的字节数。如果
recv
在
copy
时出错,那么它返回
SOCKET_ERROR
;如果
recv
函数在等待协议接收数据时
网络
中断了,那么它返回
0
。
注意:在
Unix
系统下,如果
recv
函数在等待协议接收数据时
网络
断开了,那么调用
recv
的进程会接收到一个
SIGPIPE
信号,进程对该信号的默认处理是进程终止。
七、
bind
函数
int bind(SOCKET s, const struct sockaddr FAR *name, int namelen );
当创建了一个
Socket
以后,套接字数据结构中有一个默认的
IP
地址和默认的端口号。一个服务程序必须调用
bind
函数来给其绑定一个
IP
地址和一个特定的端口号。客户程序一般不必调用
bind
函数来为其
Socket
绑定
IP
地址和断口号。该函数的第一个参数指定待绑定的
Socket
描述符;第二个参数指定一个
sockaddr
结构,该结构是这样定义的:
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
sa_family
指定地址族,对于
TCP/IP
协议族的套接字,给其置
AF_INET
。当对
TCP/IP
协议族的套接字进行绑定时,我们通常使用另一个地址结构:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其中
sin_family
置
AF_INET
;
sin_port
指明端口号;
sin_addr
结构体中只有一个唯一的字段
s_addr
,表示
IP
地址,该字段是一个整数,一般用函数
inet_addr
()把字符串形式的
IP
地址转换成
unsigned long
型的整数值后再置给
s_addr
。有的服务器是多宿主机,至少有两个网卡,那么运行在这样的服务器上的服务程序在为其
Socket
绑定
IP
地址时可以把
htonl(INADDR_ANY)
置给
s_addr
,这样做的好处是不论哪个网段上的客户程序都能与该服务程序通信;如果只给运行在多宿主机上的服务程序的
Socket
绑定一个固定的
IP
地址,那么就只有与该
IP
地址处于同一个网段上
Windows Socket1.1
程序设计
一、简介
Windows Sockets
是从
Berkeley Sockets
扩展而来的,其在继承
Berkeley Sockets
的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合
WINDOWS
消息驱动特性的
网络
事件异步选择机制。
Windows Sockets
由两部分组成:开发组件和运行组件。
开发组件:
Windows Sockets
实现文档、应用程序接口
(API)
引入库和一些头文件。
运行组件:
Windows Sockets
应用程序接口的动态链接库
(WINSOCK.DLL)
。
二、主要扩充说明
1
、异步选择机制:
Windows Sockets
的异步选择函数提供了消息机制的
网络
事件选择,当使用它登记
网络
事件发生时,应用程序相应窗口函数将收到一个消息,消息中指示了发生的
网络
事件,以及与事件相关的一些信息。
Windows Sockets
提供了一个异步选择函数
WSAAsyncSelect()
,用它来注册应用程序感兴趣的
网络
事件,当这些事件发生时,应用程序相应的窗口函数将收到一个消息。
函数结构如下
:
int PASCAL FAR WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg,long lEvent);
参数说明:
hWnd
:窗口句柄
;
wMsg
:需要发送的消息
;
lEvent
:事件(以下为事件的内容)值含义:
FD_READ
期望在套接字上收到数据(即读准备好)时接到通知
FD_WRITE
期望在套接字上可发送数据(即写准备好)时接到通知
FD_OOB
期望在套接字上有带外数据到达时接到通知
FD_ACCEPT
期望在套接字上有外来连接时接到通知
FD_CONNECT
期望在套接字连接建立完成时接到通知
FD_CLOSE
期望在套接字关闭时接到通知
例如:我们要在套接字读准备好或写准备好时接到通知,语句如下:
rc = WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE);
如果我们需要注销对套接字
网络
事件的消息发送,只要将
lEvent
设置为
0
2
、异步请求函数
在
Berkeley Sockets
中请求服务是阻塞的,
WINDOWS SICKETS
除了支持这一类函数外,还增加了相应的异步请求函数
(WSAAsyncGetXByY();)
。
3
、阻塞处理方法
Windows Sockets
为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃
CPU
让其它应用程序运行,它在调用处于阻塞时便进入一个叫“
HOOK
”的例程,此例程负责接收和分配
WINDOWS
消息,使得其它应用程序仍然能够接收到自己的消息并取得控制权。
WINDOWS
是非抢先的多任务环境,即若一个程序不主动放弃其控制权,别的程序就不能执行。因此在设计
Windows Sockets
程序时,尽管系统支持阻塞操作,但还是反对程序员使用该操作。但由于
SUN
公司下的
Berkeley Sockets
的套接字默认操作是阻塞的,
WINDOWS
作为移植的
SOCKETS
也不可避免对这个操作支持。
在
Windows Sockets
实现中,对于不能立即完成的阻塞操作做如下处理:
DLL
初始化→循环操作。在循环中,它发送任何
WINDOWS
消息,并检查这个
Windows Sockets
调用是否完成,在必要时,它可以放弃
CPU
让其它应用程序执行(当然使用超线程的
CPU
就不会有这个麻烦了
^_^
)。我们可以调用
WSACancelBlockingCall()
函数取消此阻塞操作。
在
Windows Sockets
中,有一个默认的阻塞处理例程
BlockingHook()
简单地获取并发送
WINDOWS
消息。如果要对复杂程序进行处理,
Windows Sockets
中还有
WSASetBlockingHook()
提供用户安装自己的阻塞处理例程能力;与该函数相对应的则是
SWAUnhookBlockingHook()
,它用于删除先前安装的任何阻塞处理例程,并重新安装默认的处理例程。请注意,设计自己的阻塞处理例程时,除了函数
WSACancelBlockingHook()
之外,它不能使用其它的
Windows Sockets API
函数。在处理例程中调用
WSACancelBlockingHook()
函数将取消处于阻塞的操作,它将结束阻塞循环。
4
、出错处理
Windows Sockets
为了和以后多线程环境(
WINDOWS/UNIX
)兼容,它提供了两个出错处理函数来获取和设置当前线程的最近错误号。(
WSAGetLastEror()
和
WSASetLastError()
)
5
、启动与终止
使用函数
WSAStartup()
和
WSACleanup()
启动和终止套接字。
三、
Windows Sockets网络
程序设计核心
我们终于可以开始真正的
Windows Sockets 网络
程序设计了。不过我们还是先看一看每个
Windows Sockets 网络
程序都要涉及的内容。让我们一步步慢慢走。
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);
if((len == SOCKET_ERROR)&&(WSAGetLastError()==WSAWOULDBLOCK)){...}
实例应用:
基于
Visual C++
的
Winsock API
研究
为了方便
网络编程
,
90
年代初,由
Microsoft
联合了其他几家公司共同制定了一套
WINDOWS
下的
网络编程
接口,即
Windows Sockets
规范,它不是一种
网络
协议
,
而是一套开放的、支持多种协议的
Windows
下的
网络编程
接口。现在的
Winsock
已经基本上实现了与协议无关,你可以使用
Winsock
来调用多种协议的功能,但较常使用的是
TCP/IP
协议。
Socket
实际在计算机中提供了一个通信端口,可以通过这个端口与任何一个具有
Socket
接口的计算机通信。应用程序在
网络
上传输,接收的信息都通过这个
Socket
接口来实现。
微软为
VC
定义了
Winsock
类如
CAsyncSocket
类和派生于
CAsyncSocket
的
CSocket
类,它们简单易用,读者朋友当然可以使用这些类来实现自己的
网络
程序,但是为了更好的了解
Winsock API编程
技术,我们这里探讨怎样使用底层的
API
函数实现简单的
Winsock 网络
应用程式设计,分别说明如何在
Server
端和
Client
端操作
Socket
,实现基于
TCP/IP
的数据传送,最后给出相关的源代码。
在
VC
中进行
WINSOCK
的
API编程
开发的时候,需要在项目中使用下面三个文件,否则会出现编译错误。
1
.
WINSOCK.H:
这是
WINSOCK API
的头文件,需要包含在项目中。
2
.
WSOCK32.LIB: WINSOCK API
连接库文件。在使用中,一定要把它作为项目的非缺省的连接库包含到项目文件中去。
3
.
WINSOCK.DLL: WINSOCK
的动态连接库,位于
WINDOWS
的安装目录下。
一、服务器端操作
socket
(套接字)
1)
在初始化阶段调用
WSAStartup()
此函数在应用程序中初始化
Windows Sockets DLL
,只有此函数调用成功后,应用程序才可以再调用其他
Windows Sockets DLL
中的
API
函数。在程式中调用该函数的形式如下:
WSAStartup((WORD)((1<<8|1)
,(
LPWSADATA
)
&WSAData)
,其中
(1<<8|1)
表示我们用的是
WinSocket1.1
版本,
WSAata
用来存储系统传回的关于
WinSocket
的资料。
2)
建立
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
。
3)
绑定端口
接下来要为服务器端定义的这个监听的
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()
函数来获知其被设定的值。
4
)监听
当服务器端的
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;
…
}
5
)服务器端接受客户端的连接请求
当
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
的长度
6
)结束
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
的操作
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 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
对象,但要注意连接数不要大于五。
由于
Socket API
函数还有许多,如获取远端服务器、本地客户机的
IP
地址、主机名等等,读者可以再此基础上对
CMySocket
补充完善,实现更多的功能。
TCP/IP Winsock编程
要点
利用
Winsock编程
由同步和异步方式,同步方式逻辑清晰,
编程
专注于应用,在抢先式的多任务操作系统中
(WinNt
、
Win2K)
采用多线程方式效率基本达到异步方式的水平,应此以下为同步方式
编程
要点。
1
、快速通信
Winsock
的
Nagle
算法将降低小数据报的发送速度,而系统默认是使用
Nagle
算法
,
使用
int setsockopt(SOCKET s, int level, int optname, const char FAR *optval, int optlen );
函数关闭它
例子:
SOCKET sConnect;
sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
int bNodelay = 1;
int err; err = setsockopt( sConnect, IPPROTO_TCP, TCP_NODELAY, (char *)&bNodelay, sizoeof(bNodelay));//
不采用延时算法
if (err != NO_ERROR)
TRACE ("setsockopt failed for some reason/n");
2
、
SOCKET
的
SegMentSize
和收发缓冲
TCPSegMentSize
是发送接受时单个数据报的最大长度,系统默认为
1460
,收发缓冲大小为
8192
。
在
SOCK_STREAM
方式下,如果单次发送数据超过
1460
,系统将分成多个数据报传送,在对方接受到的将是一个数据流,应用程序需要增加断帧的判断。当然可以采用修改注册表的方式改变
1460
的大小,但
MicrcoSoft
认为
1460
是最佳效率的参数,不建议修改。
在工控系统中,建议关闭
Nagle
算法,每次发送数据小于
1460
个字节(推荐
1400
),这样每次发送的是一个完整的数据报,减少对方对数据流的断帧处理。
3
、同步方式中减少断网时
connect
函数的阻塞时间
同步方式中的断网时
connect
的阻塞时间为
20
秒左右,可采用
gethostbyaddr
事先判断到服务主机的路径是否是通的,或者先
ping
一下对方主机的
IP
地址。
A
、采用
gethostbyaddr
阻塞时间不管成功与否为
4
秒左右。
例子:
LONG lPort=3024;
struct sockaddr_in ServerHostAddr;//
服务主机地址
ServerHostAddr.sin_family=AF_INET;
ServerHostAddr.sin_port=::htons(u_short(lPort));
ServerHostAddr.sin_addr.s_addr=::inet_addr("192.168.1.3");
HOSTENT* pResult=gethostbyaddr((const char *) & (ServerHostAddr.sin_addr. s_addr), 4, AF_INET);
if(NULL==pResult)
{
int nErrorCode=WSAGetLastError();
TRACE("gethostbyaddr errorcode=%d",nErrorCode);
}
else
{
TRACE("gethostbyaddr %s/n",pResult->h_name);;
}
B
、采用
PING
方式时间约
2
秒左右
暂略
4
、同步方式中解决
recv
,
send
阻塞问题
采用
select
函数解决,在收发前先检查读写可用状态。
A
、读
例子:
TIMEVAL tv01 = {0, 1};//1ms
钟延迟,实际为0-10毫秒
int
nSelectRet;
int
nErrorCode;
FD_SET fdr = {1, sConnect};
nSelectRet=::select(0, &fdr, NULL, NULL, &tv01);//
检查可读状态
if
(SOCKET_ERROR==nSelectRet)
{
nErrorCode=WSAGetLastError();
TRACE("select read status errorcode=%d",nErrorCode);
::closesocket(sConnect);
goto //
重新连接(客户方),或服务线程退出(服务方);
}
if
(nSelectRet==0)//
超时发生,无可读数据
{
继续查读状态或向对方主动发送
}
else
{
//
读数据
}
B
、写
TIMEVAL tv01 = {0, 1};//1ms
钟延迟,实际为9-10毫秒
int
nSelectRet;
int
nErrorCode;
FD_SET fdw = {1, sConnect};
nSelectRet=::select(0, NULL, NULL,&fdw, &tv01);//
检查可写状态
if
(SOCKET_ERROR==nSelectRet)
{
nErrorCode=WSAGetLastError();
TRACE("select write status errorcode=%d",nErrorCode);
::closesocket(sConnect);
//goto
重新连接(客户方),或服务线程退出(服务方);
}
if
(nSelectRet==0)//
超时发生,缓冲满或网络忙
{
//
继续查写状态或查读状态
}
else
{
//
发送
}
5
、改变TCP收发缓冲区大小
系统默认为8192,利用如下方式可改变。
SOCKET sConnect;
sConnect=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
int nrcvbuf=1024*20;
int err=setsockopt(sConnect, SOL_SOCKET, SO_SNDBUF,/*
写缓冲,读缓冲为SO_RCVBUF*/
(char *)&nrcvbuf, sizeof(nrcvbuf));
if (err != NO_ERROR)
{
TRACE("setsockopt Error!/n");
}
//
在设置缓冲时,检查是否真正设置成功用
int getsockopt(SOCKET s, int level, int optname, char FAR *optval, int FAR *optlen );
6
、服务方同一端口多
IP
地址的
bind
和
listen
在可靠性要求高的应用中,要求使用双网和多
网络
通道,再服务方很容易实现,用如下方式可建立客户对本机所有
IP
地址在端口
3024
下的请求服务。
/
SOCKET hServerSocket_DS = INVALID_SOCKET;
struct
sockaddr_in HostAddr_DS;//
服务器主机地址
LONG lPort = 3024;
HostAddr_DS.sin_family = AF_INET;
HostAddr_DS.sin_port = ::htons(u_short(lPort));
HostAddr_DS.sin_addr.s_addr = htonl(INADDR_ANY);
hServerSocket_DS = ::socket( AF_INET, SOCK_STREAM,IPPROTO_TCP);
if
(hServerSocket_DS == INVALID_SOCKET)
{
AfxMessageBox("
建立数据服务器SOCKET 失败!");
return FALSE;
}
if
(SOCKET_ERROR == ::bind(hServerSocket_DS,(struct sockaddr *)(&(HostAddr_DS)),sizeof(SOCKADDR)))
{
int nErrorCode = WSAGetLastError ();
TRACE("bind error=%d/n",nErrorCode);
AfxMessageBox("Socket Bind
错误!");
return FALSE;
}
if
(SOCKET_ERROR == ::listen(hServerSocket_DS,10))//10
个客户
{
AfxMessageBox("Socket listen
错误!");
return FALSE;
}
AfxBeginThread(ServerThreadProc,NULL,THREAD_PRIORITY_NORMAL);
在客户方要复杂一些,连接断后,重联不成功则应换下一个
IP
地址连接。也可采用同时连接好后备用的方式。
7
、用
TCP/IP Winsock
实现变种
Client/Server
传统的
Client/Server
为客户问、服务答,收发是成对出现的。而变种的
Client/Server
是指在连接时有客户和服务之分,建立好通信连接后,不再有严格的客户和服务之分,任何方都可主动发送,需要或不需要回答看应用而言,这种方式在工控行业很有用,比如
RTDB
作为
I/O Server
的客户,但
I/O Server
也可主动向
RTDB
发送开关状态变位、随即事件等信息。在很大程度上减少了
网络
通信负荷、提高了效率。
采用
1-6
的
TCP/IP编程
要点,在
Client
和
Server
方均已接收优先,适当控制时序就能实现。
Windows Sockets API
实现
网络
异步通讯
摘要:本文对如何使用面向连接的流式套接字实现对网卡的
编程
以及如何实现异步
网络
通讯等问题进行了讨论与阐述。
一、
引言
在
80
年代初,美国加利福尼亚大学伯克利分校的研究人员为
TCP/IP网络
通信开发了一个专门用于
网络
通讯开发的
API
。这个
API
就是
Socket
接口(套接字)
--
当今在
TCP/IP网络
最为通用的一种
API
,也是在互联网上进行应用开发最为通用的一种
API
。在微软联合其它几家公司共同制定了一套
Windows
下的
网络编程
接口
Windows Sockets
规范后,由于在其规范中引入了一些异步函数,增加了对
网络
事件异步选择机制,因此更加符合
Windows
的消息驱动特性,使
网络
开发人员可以更加方便的进行高性能
网络
通讯程序的设计。本文接下来就针对
Windows Sockets API
进行面向连接的流式套接字
编程
以及对异步
网络
通讯的
编程
实现等问题展开讨论。
二、
面向连接的流式套接字
编程
模型的设计
本文在方案选择上采用了在
网络编程
中最常用的一种模型
--
客户机
/
服务器模型。这种客户
/
服务器模型是一种非对称式
编程
模式。该模式的基本思想是把集中在一起的应用划分成为功能不同的两个部分,分别在不同的计算机上运行,通过它们之间的分工合作来实现一个完整的功能。对于这种模式而言其中一部分需要作为服务器,用来响应并为客户提供固定的服务;另一部分则作为客户机程序用来向服务器提出请求或要求某种服务。
本文选取了基于
TCP/IP
的客户机
/
服务器模型和面向连接的流式套接字。其通信原理为:服务器端和客户端都必须建立通信套接字,而且服务器端应先进入监听状态,然后客户端套接字发出连接请求,服务器端收到请求后,建立另一个套接字进行通信,原来负责监听的套接字仍进行监听,如果有其它客户发来连接请求,则再建立一个套接字。默认状态下最多可同时接收
5
个客户的连接请求,并与之建立通信关系。因此本程序的设计流程应当由服务器首先启动,然后在某一时刻启动客户机并使其与服务器建立连接。服务器与客户机开始都必须调用
Windows Sockets API
函数
socket()
建立一个套接字
sockets,
然后服务器方调用
bind()
将套接字与一个本地
网络
地址捆扎在一起,再调用
listen()
使套接字处于一种被动的准备接收状态,同时规定它的请求队列长度。在此之后服务器就可以通过调用
accept()
来接收客户机的连接。
相对于服务器,客户端的工作就显得比较简单了,当客户端打开套接字之后,便可通过调用
connect()
和服务器建立连接。连接建立之后,客户和服务器之间就可以通过连接发送和接收资料。最后资料传送结束,双方调用
closesocket()
关闭套接字来结束这次通讯。整个通讯过程的具体流程框图可大致用下面的流程图来表示:
面向连接的流式套接字
编程
流程示意图
三、
软件
设计要点以及异步通讯的实现
根据前面设计的程序流程,可将程序划分为两部分:服务器端和客户端。而且整个实现过程可以大致用以下几个非常关键的
Windows Sockets API
函数将其惯穿下来:
服务器方:
socket()->bind()->listen->accept()->recv()/send()->closesocket()
客户机方:
socket()->connect()->send()/recv()->closesocket()
有鉴于以上几个函数在整个
网络编程
中的重要性,有必要结合程序实例对其做较深入的剖析。服务器端应用程序在使用套接字之前,首先必须拥有一个
Socket
,系统调用
socket()
函数向应用程序提供创建套接字的手段。该套接字实际上是在计算机中提供了一个通信埠,可以通过这个埠与任何一个具有套接字接口的计算机通信。应用程序在
网络
上传输、接收的信息都通过这个套接字接口来实现的。在应用开发中如同使用文件句柄一样,可以对套接字句柄进行读写操作:
sock=socket(AF_INET,SOCK_STREAM,0);
函数的第一个参数用于指定地址族,在
Windows
下仅支持
AF_INET(TCP/IP
地址
)
;第二个参数用于描述套接字的类型,对于流式套接字提供有
SOCK_STREAM
;最后一个参数指定套接字使用的协议,一般为
0
。该函数的返回值保存了新套接字的句柄,在程序退出前可以用
closesocket(sock);
函数来将其释放。服务器方一旦获取了一个新的套接字后应通过
bind()
将该套接字与本机上的一个端口相关联:
sockin.sin_family=AF_INET;
sockin.sin_addr.s_addr=0;
sockin.sin_port=htons(USERPORT);
bind(sock,(LPSOCKADDR)&sockin,sizeof(sockin)));
该函数的第二个参数是一个指向包含有本机
IP
地址和端口信息的
sockaddr_in
结构类型的指针,其成员描述了本地端口号和本地主机地址,经过
bind()
将服务器进程在
网络
上标识出来。需要注意的是由于
1024
以内的埠号都是保留的埠号因此如无特别需要一般不能将
sockin.sin_port
的埠号设置为
1024
以内的值。然后调用
listen()
函数开始侦听,再通过
accept()
调用等待接收连接以完成连接的建立:
//
连接请求队列长度为
1
,即只允许有一个请求
,
若有多个请求
,
//
则出现错误,给出错误代码
WSAECONNREFUSED
。
listen(sock,1);
//
开启线程避免主程序的阻塞
AfxBeginThread(Server,NULL);
……
UINT Server(LPVOID lpVoid)
{
……
int nLen=sizeof(SOCKADDR);
pView->newskt=accept(pView->sock,(LPSOCKADDR)& pView->sockin,(LPINT)& nLen);
……
WSAAsyncSelect(pView->newskt,pView->m_hWnd,WM_SOCKET_MSG,FD_READ|FD_CLOSE);
return 1;
}
这里之所以把
accept()
放到一个线程中去是因为在执行到该函数时如没有客户连接服务器的请求到来,服务器就会停在
accept
语句上等待连接请求的到来,这势必会引起程序的阻塞,虽然也可以通过设置套接字为非阻塞方式使在没有客户等待时可以使
accept
()函数调用立即返回,但这种轮询套接字的方式会使
CPU
处于忙等待方式,从而降低程序的运行效率大大浪费系统资源。考虑到这种情况,将套接字设置为阻塞工作方式,并为其单独开辟一个子线程,将其阻塞控制在子线程范围内而不会造成整个应用程序的阻塞。对于
网络
事件的响应显然要采取异步选择机制,只有采取这种方式才可以在由
网络
对方所引起的不可预知的
网络
事件发生时能马上在进程中做出及时的响应处理,而在没有
网络
事件到达时则可以处理其他事件,这种效率是很高的,而且完全符合
Windows
所标榜的消息触发原则。前面那段代码中的
WSAAsyncSelect()
函数便是实现
网络
事件异步选择的核心函数。
通过第四个参数注册应用程序感兴取的
网络
事件,在这里通过
FD_READ|FD_CLOSE
指定了
网络
读和
网络
断开两种事件,当这种事件发生时变会发出由第三个参数指定的自定义消息
WM_SOCKET_MSG
,接收该消息的窗口通过第二个参数指定其句柄。在消息处理函数中可以通过对消息参数低字节进行判断而区别出发生的是何种
网络
事件:
void
CNetServerView::OnSocket(WPARAM wParam,LPARAM lParam)
{
int iReadLen=0;
int message=lParam & 0x0000FFFF;
switch(message)
{
case FD_READ://
读事件发生。此时有字符到达,需要进行接收处理
char cDataBuffer[MTU*10];
//
通过套接字接收信息
iReadLen = recv(newskt,cDataBuffer,MTU*10,0);
//
将信息保存到文件
if(!file.Open("ServerFile.txt",CFile::modeReadWrite))
file.Open("E:ServerFile.txt",CFile::modeCreate|CFile::modeReadWrite);
file.SeekToEnd();
file.Write(cDataBuffer,iReadLen);
file.Close();
break;
case FD_CLOSE://
网络断开事件发生。此时客户机关闭或退出。
……//进行相应的处理
break;
default:
break;
}
}
//
在这里需要实现对自定义消息WM_SOCKET_MSG的响应,需要在头文件和实现文件中分别添加其消息映射关系:
//
头文件:
//{{AFX_MSG(CNetServerView)
//}}AFX_MSG
void
OnSocket(WPARAM wParam,LPARAM lParam);
DECLARE_MESSAGE_MAP()
//
实现文件:
BEGIN_MESSAGE_MAP(CNetServerView, CView)
//{{AFX_MSG_MAP(CNetServerView)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_SOCKET_MSG,OnSocket)
END_MESSAGE_MAP()
在进行异步选择使用
WSAAsyncSelect()
函数时,有以下几点需要引起特别的注意:
1
.
连续使用两次
WSAAsyncSelect()
函数时,只有第二次设置的事件有效,如:
WSAAsyncSelect(s,hwnd,wMsg1,FD_READ);
WSAAsyncSelect(s,hwnd,wMsg2,FD_CLOSE);
这样只有当
FD_CLOSE
事件发生时才会发送
wMsg2
消息。
2
.可以在设置过异步选择后通过再次调用
WSAAsyncSelect(s,hwnd,0,0);
的形式取消在套接字上所设置的异步事件。
3
.
Windows Sockets DLL
在一个
网络
事件发生后,通常只会给相应的应用程序发送一个消息,而不能发送多个消息。但通过使用一些函数隐式地允许重发此事件的消息,这样就可能再次接收到相应的消息。
4
.在调用过
closesocket()
函数关闭套接字之后不会再发生
FD_CLOSE
事件。
以上基本完成了服务器方的程序设计,下面对于客户端的实现则要简单多了,在用
socket()
创建完套接字之后只需通过调用
connect()
完成同服务器的连接即可,剩下的工作同服务器完全一样:用
send()/recv()
发送
/
接收收据,用
closesocket()
关闭套接字:
sockin.sin_family=AF_INET; //
地址族
sockin.sin_addr.S_un.S_addr=IPaddr; //
指定服务器的
IP
地址
sockin.sin_port=m_Port; //
指定连接的端口号
int nConnect=connect(sock,(LPSOCKADDR)&sockin,sizeof(sockin));
本文采取的是可靠的面向连接的流式套接字。在数据发送上有
write()
、
writev()
和
send()
等三个函数可供选择,其中前两种分别用于缓冲发送和集中发送,而
send()
则为可控缓冲发送,并且还可以指定传输控制标志为
MSG_OOB
进行带外数据的发送或是为
MSG_DONTROUTE
寻径控制选项。在信宿地址的
网络
号部分指定数据发送需要经过的
网络
接口,使其可以不经过本地寻径机制直接发送出去。这也是其同
write()
函数的真正区别所在。由于接收数据系统调用和发送数据系统调用是一一对应的,因此对于数据的接收,在此不再赘述,相应的三个接收函数分别为:
read()
、
readv()
和
recv()
。由于后者功能上的全面,本文在实现上选择了
send()-recv()
函数对,在具体
编程
中应当视具体情况的不同灵活选择适当的发送
-
接收函数对。
小结:
TCP/IP
协议是目前各
网络
操作系统主要的通讯协议,也是
Internet
的通讯协议,本文通过
Windows Sockets API
实现了对基于
TCP/IP
协议的面向连接的流式套接字
网络
通讯程序的设计,并通过异步通讯和多线程等手段提高了程序的运行效率,避免了阻塞的发生。用
VC++6.0
的
Sockets API
实现一个聊天室程序
1.VC++网络编程
及
Windows Sockets API
简介
VC++
对
网络编程
的支持有
socket
支持,
WinInet
支持,
MAPI
和
ISAPI
支持等。其中,
Windows Sockets API
是
TCP/IP网络
环境里,也是
Internet
上进行开发最为通用的
API
。最早美国加州大学
Berkeley
分校在
UNIX
下为
TCP/IP
协议开发了一个
API
,这个
API
就是著名的
Berkeley Socket
接口
(
套接字
)
。在桌面操作系统进入
Windows
时代后,仍然继承了
Socket
方法。在
TCP/IP网络
通信环境下,
Socket
数据传输是一种特殊的
I/O
,它也相当于一种文件描述符,具有一个类似于打开文件的函数调用
-socket()
。可以这样理解:
Socket
实际上是一个通信端点,通过它,用户的
Socket
程序可以通过
网络
和其他的
Socket
应用程序通信。
Socket
存在于一个
"
通信域
"(
为描述一般的线程如何通过
Socket
进行通信而引入的一种抽象概念
)
里,并且与另一个域的
Socket
交换数据。
Socket
有三类。第一种是
SOCK_STREAM(
流式
)
,提供面向连接的可靠的通信服务,比如
telnet,http
。第二种是
SOCK_DGRAM(
数据报
)
,提供无连接不可靠的通信,比如
UDP
。第三种是
SOCK_RAW(
原始
)
,主要用于协议的开发和测试,支持通信底层操作,比如对
IP
和
ICMP
的直接访问。
2.Windows Socket
机制分析
2.1
一些基本的
Socket
系统调用
主要的系统调用包括:
socket()-
创建
Socket
;
bind()-
将创建的
Socket
与本地端口绑定;
connect()
与
accept()-
建立
Socket
连接;
listen()-
服务器监听是否有连接请求;
send()-
数据的可控缓冲发送;
recv()-
可控缓冲接收;
closesocket()-
关闭
Socket
。
2.2Windows Socket
的启动与终止
启动函数
WSAStartup()
建立与
Windows Sockets DLL
的连接,终止函数
WSAClearup()
终止使用该
DLL
,这两个函数必须成对使用。
2.3
异步选择机制
Windows
是一个非抢占式的操作系统,而不采取
UNIX
的阻塞机制。当一个通信事件产生时,操作系统要根据设置选择是否对该事件加以处理,
WSAAsyncSelect()
函数就是用来选择系统所要处理的相应事件。当
Socket
收到设定的
网络
事件中的一个时,会给程序窗口一个消息,这个消息里会指定产生
网络
事件的
Socket
,发生的事件类型和错误码。
2.4
异步数据传输机制
WSAAsyncSelect()
设定了
Socket
上的须响应通信事件后,每发生一个这样的事件就会产生一个
WM_SOCKET
消息传给窗口。而在窗口的回调函数中就应该添加相应的数据传输处理代码。
3.
聊天室程序的设计说明
3.1
实现思想
在
Internet
上的聊天室程序一般都是以服务器提供服务端连接响应,使用者通过客户端程序登录到服务器,就可以与登录在同一服务器上的用户交谈,这是一个面向连接的通信过程。因此,程序要在
TCP/IP
环境下,实现服务器端和客户端两部分程序。
3.2
服务器端工作流程
服务器端通过
socket()
系统调用创建一个
Socket
数组后
(
即设定了接受连接客户的最大数目
)
,与指定的本地端口绑定
bind()
,就可以在端口进行侦听
listen()
。如果有客户端连接请求,则在数组中选择一个空
Socket
,将客户端地址赋给这个
Socket
。然后登录成功的客户就可以在服务器上聊天了。
3.3
客户端工作流程
客户端程序相对简单,只需要建立一个
Socket
与服务器端连接,成功后通过这个
Socket
来发送和接收数据就可以了。
4.
核心代码分析
限于篇幅,这里仅给出与
网络编程
相关的核心代码,其他的诸如聊天文字的服务器和客户端显示读者可以自行添加。
4.1
服务器端代码
开启服务器功能
:
void
OnServerOpen() //
开启服务器功能
{
WSADATA wsaData;
int iErrorCode;
char chInfo[64];
if (WSAStartup(WINSOCK_VERSION, &wsaData)) //
调用Windows Sockets DLL
{
MessageBeep(MB_ICONSTOP);
MessageBox("Winsock
无法初始化!", AfxGetAppName(), MB_OK|MB_ICONSTOP);
WSACleanup();
return;
}
else
WSACleanup();
if (gethostname(chInfo, sizeof(chInfo)))
{
ReportWinsockErr("/n
无法获取主机!/n ");
return;
}
CString csWinsockID = "/n==>>
服务器功能开启在端口:No. ";
csWinsockID += itoa(m_pDoc->m_nServerPort, chInfo, 10);
csWinsockID += "/n";
PrintString(csWinsockID); //
在程序视图显示提示信息的函数,读者可自行创建
m_pDoc->m_hServerSocket=socket(PF_INET, SOCK_STREAM, DEFAULT_PROTOCOL);
//
创建服务器端Socket,类型为SOCK_STREAM,面向连接的通信
if (m_pDoc->m_hServerSocket == INVALID_SOCKET)
{
ReportWinsockErr("
无法创建服务器socket!");
return;
}
m_pDoc->m_sockServerAddr.sin_family = AF_INET;
m_pDoc->m_sockServerAddr.sin_addr.s_addr = INADDR_ANY;
m_pDoc->m_sockServerAddr.sin_port = htons(m_pDoc->m_nServerPort);
if (bind(m_pDoc->m_hServerSocket, (LPSOCKADDR)&m_pDoc->m_sockServerAddr,sizeof(m_pDoc->m_sockServerAddr)) == SOCKET_ERROR) //
与选定的端口绑定
{
ReportWinsockErr("
无法绑定服务器socket!");
return;
}
iErrorCode=WSAAsyncSelect(m_pDoc->m_hServerSocket,m_hWnd, WM_SERVER_ACCEPT, FD_ACCEPT);
//
设定服务器相应的网络事件为FD_ACCEPT,即连接请求,
//
产生相应传递给窗口的消息为WM_SERVER_ACCEPT
if (iErrorCode == SOCKET_ERROR)
{
ReportWinsockErr("WSAAsyncSelect
设定失败!");
return;
}
if (listen(m_pDoc->m_hServerSocket, QUEUE_SIZE) == SOCKET_ERROR) //
开始监听客户连接请求
{
ReportWinsockErr("
服务器socket监听失败!");
m_pParentMenu->EnableMenuItem(ID_SERVER_OPEN, MF_ENABLED);
return;
}
m_bServerIsOpen = TRUE; //
监视服务器是否打开的变量
return;
}
响应客户发送聊天文字到服务器:
ON_MESSAGE(WM_CLIENT_READ, OnClientRead)
LRESULT OnClientRead(WPARAM wParam, LPARAM lParam)
{
int iRead;
int iBufferLength;
int iEnd;
int iRemainSpace;
char chInBuffer[1024];
int i;
for(i=0;(i<MAXCLIENT)&&(M_ACLIENTSOCKET[I]!=WPARAM);I++)
//MAXClient是服务器可响应连接的最大数目
{
}
if(i==MAXClient)
return 0L;
iBufferLength = iRemainSpace = sizeof(chInBuffer);
iEnd = 0;
iRemainSpace -= iEnd;
iBytesRead = recv(m_aClientSocket[i], (LPSTR)(chInBuffer+iEnd), iSpaceRemaining, NO_FLAGS);
//用可控缓冲接收函数recv()来接收字符
iEnd+=iRead;
if (iBytesRead == SOCKET_ERROR)
ReportWinsockErr("recv
出错!");
chInBuffer[iEnd] = '/0';
if (lstrlen(chInBuffer) != 0)
{
PrintString(chInBuffer); //
服务器端文字显示
OnServerBroadcast(chInBuffer); //
自己编写的函数,向所有连接的客户广播这个客户的聊天文字
}
return(0L);
}
对于客户断开连接,会产生一个FD_CLOSE消息,只须相应地用closesocket()关闭相应的Socket即可,这个处理比较简单。
4.2
客户端代码
连接到服务器:
void
OnSocketConnect()
{
WSADATA wsaData;
DWORD dwIPAddr;
SOCKADDR_IN sockAddr;
if(WSAStartup(WINSOCK_VERSION,&wsaData)) //
调用Windows Sockets DLL
{
MessageBox("Winsock
无法初始化!",NULL,MB_OK);
return;
}
m_hSocket=socket(PF_INET,SOCK_STREAM,0); //
创建面向连接的socket
sockAddr.sin_family=AF_INET; //
使用TCP/IP协议
sockAddr.sin_port=m_iPort; //
客户端指定的IP地址
sockAddr.sin_addr.S_un.S_addr=dwIPAddr;
int nConnect=connect(m_hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)); //
请求连接
if(nConnect)
ReportWinsockErr("
连接失败!");
else
MessageBox("
连接成功!",NULL,MB_OK);
int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,FD_READ); //
指定响应的事件,为服务器发送来字符
if(iErrorCode==SOCKET_ERROR)
MessageBox("WSAAsyncSelect
设定失败!");
}
接收服务器端发送的字符也使用可控缓冲接收函数
recv()
,客户端聊天的字符发送使用数据可控缓冲发送函数
send()
,这两个过程比较简单,在此就不加赘述了。
5.
小结
通过聊天室程序的编写,可以基本了解
Windows Sockets API编程
的基本过程和精要之处。本程序在
VC++6.0
下编译通过,在使用
windows 98/NT
的局域网里运行良好。
用
VC++
制作一个简单的局域网消息发送工程
本工程类似于
oicq
的消息发送机制,不过他只能够发送简单的字符串。虽然简单,但他也是一个很好的
VC网络
学习例子。
本例通过
VC
带的
SOCKET
类,重载了他的一个接受类
mysock
类,此类可以吧接收到的信息显示在客户区理。以下是实现过程:
建立一个
MFC
单文档工程,工程名为
oicq,
在第四步选取
WINDOWS SOCKetS
支持,其它取默认设置即可。为了简单,这里直接把
about
对话框作些改变,作为发送信息界面。
这里通过失去对话框来得到发送的字符串、获得焦点时把字符串发送出去。创建
oicq
类的窗口,获得
VIEW
类指针,进而可以把接收到的信息显示出来。
extern
CString bb;
void
CAboutDlg::OnKillFocus(CWnd* pNewWnd)
{
// TODO: Add your message handler code here
CDialog::OnKillFocus(pNewWnd);
bb=m_edit;
}
对于OICQVIEW类
char
aa[100];
CString mm;
CDC* pdc;
class
mysock:public CSocket //
派生mysock类,此类既有接受功能
{
public
:
void OnReceive(int nErrorCode) //
可以随时接收信息
{
CSocket::Receive((void*)aa,100,0);
mm=aa;
CString ll=" ";//
在显示消息之前,消除前面发送的消息
pdc->TextOut(50,50,ll);
pdc->TextOut(50,50,mm);
}
};
mysock sock1;
CString bb;
BOOL COicqView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
CView::OnSetFocus(pOldWnd);
// TODO: Add your message handler code here and/or call default
bb="besting:"+bb; //
确定发送者身份为besting
sock1.SendTo(bb,100,1060,"192.168.0.255",0); //
获得焦点以广播形式发送信息,端口号为1060
return CView::OnSetCursor(pWnd, nHitTest, message);
}
int
COicqView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
sock1.Create(1060,SOCK_DGRAM,NULL);//
以数据报形式发送消息
static CClientDC wdc(this); //
获得当前视类的指针
pdc=&wdc;
// TODO: Add your specialized creation code here
return 0;
}
运行一下,打开
ABOUT
对话框,输入发送信息,
enter
键就可以发送信息了,是不是有点像
qq
啊?