C语言实的串行通信接口程序
ibm公司于1994年4月推出的tcp/ip for dos v2.1.1所提供的开发软件包programmer's tool kit不仅带有dos下网络编程接口,而且提供了windows下网络异步通信接口winsock。
一、socket网络编程原理socket是bsd unix提供的网络应用编程接口,它采用客户机/服务器的通信机制,使网络客户机方和服务器方通过socket实现网络之间的连接和数据交换。socket提供了一系列的系统调用,使用这些系统调用可以实现tcp、udp、icmp和ip等多种网络协议之间的通信。
socket有三种主要类型:stream sockets,datagram sockets和raw sockets。streamsocket接口定义了一种可靠的面向连接的服务,它实现了无差错无重复的顺序数据传输。它通过内置的流量控制解决了数据的拥塞,应用程序可以发送任意长度的数据,将数据当作字节流。datagram socket接口定义了一种无连接的服务,数据通过相互独立的包进行传输,包的传输是无序的,并且不保证是否出错、丢失和重复。包长度是有限的(隐含长度为8192字节,最大长度可设为32768字节)。raw socket接口允许对低层协议如ip和icmp的直接存取,它主要用于新的网络协议实现的测试等。
下面我们通过一个面向连接的传输发生的典型情况来说明socket网络通信的实现。
由图我们可以看出,客户机和服务器的关系不是对称的。服务器首先启动,然后在某一时间启动客户机与服务器建立连接。服务器和客户机开始都必须用调用socket ()建立一个套接字(socket),然后服务器调用bind()将套接字与一个本地网络地址捆扎在一起,再用调用()使套接字处于一种被动的准备接收状态,同时规定它的请求队列长度,之后服务器就可以调用accept()来接收连接了。客户机在建立套接字之后,便可以通过调用connect()和服务器建立连接。连接建立后,客户机和服务器之间就可以通过连接发送和接收数据(调用read()和write())。最后,待数据传送结束,双方调用close()关闭套接字。
@@t8s10700.gif;面向连接的协议实现的socket调用图@@
二、winsock对socket的扩充
bsd socket支持阻塞(blocking)和非阻塞(non-blocking)两种工作方式。在阻塞方式下,connect()、accept()、read()和recv()等调用在执行时都处于阻塞状态直到它成功或出错返回。在非阻塞方式下,这些调用是立即返回的,但是它们是否完成得靠查询才能知道。对于windows这种非抢先多任务操作系统来说,这两种工作方式都是难以接受的,为此,winsock在尽量与bsd socket保持一致的前提下,又对它作了必要的扩充。
winsock对bsd socket的扩充主要是在基于消息、对网络事件的异步存取接口上。表1列出了winsock扩充的函数功能。
从表1可以看出,winsock的扩充功能可以分为如下几类。
(1)异步选择机制
异步选择函数wsaasyncselect()允许应用程序提名一个或多个感兴趣的网络事件,所有非阻塞的网络i/o例程(如send()和resv()),不管它是已经使用还是即将使用,都可作为wsaasyncselect()函数选择的候选。当被提名的网络事件发生时,windows应用程序的窗口函数将收到一个消息,消息附带的参数指示被提名过的某一网络事件。
@@t8s10701.gif;表1 winsock扩充函数功能@@
(2)异步请求例程
异步请求例程允许应用程序用异步方式获取请求的信息,如wsaasyncgetxbyy()类函数允许用户请求异步服务,这些功能在使用标准berkeley函数时是阻塞的。函数wsacancelasyncrequest()允许用户终止一个正在执行的异步请求。
(3)阻塞处理方法
winsock在调用处于阻塞时进入一个叫“hook”的例程,它负责处理windows消息,使得windows的消息循环能够继续。winsock还提供了两个函数(wsasetblockinghook()和wsaunhookblockinghook())让用户能够设置和取消自己的阻塞处理例程。另外,函数wsaisblocking()可以检测调用是否阻塞,函数wsacancelblockingcall()可以取消一个阻塞的调用。
(4)出错处理
为了和以后的多线索环境(如windows nt)兼容,winsock提供了两个出错处理函数wsagetlasterror()和wsasetlasterror()来获取和设置本线索的最近错误号。
(5)启动与终止
winsock的应用程序在使用上述winsock函数前,必须先调用wsastartup()函数对windows sockets dll进行初始化,以协商winsock的版本支持,并分配必要的资源。在应用程序退出之前,应该先调用函数wsacleanup()终止对windows sockets dll的使用,并释放资源,以利下一次使用。
在这些函数中,实现windows网络实时通信的关键是异步选择函数wsaasyncselect()的使用,其原型如下:int pascal far wsaasyncselect(socket s,hwnd hwnd, unsigned intwmsg, long levent);它请求windows sockets dll在检测到在套接字s上发生的levent事件时,向窗口hwnd发送一个消息wmsg。它自动地设置套接字s处于非阻塞工作方式。参数levent由表2所列事件的一个或多个组成。
@@t8s10702.gif;表2 异步选择网络事件@@
例如,我们要在套接字s读准备好或写准备好时接到通知,可以使用下面的语句:
rc=wsaasyncselect(s,hwnd,wmsg,fd-read | fd-write);
当套接字s上被提名的一个网络事件发生时,窗口hwnd将收到消息wmsg,变量lparam的低字指示网络发生的事件,高字指示错误码。应用程序就可以通过这些信息来决定自己的下一步动作。
三、网络实时通信的实现
我们来设计一个简单的基于连接的点对点网络实时通信程序。服务器首先启动,它建立套接字之后等待客户机的连接;客户机在启动后,建立套接字,然后和服务器建立连接;连接建立后,客户机通过连接给服务器发送一段数据,服务器接收后又将它发送回来,客户机再发送,如此循环,直至用户命令客户机退出或网络出错;客户机关闭连接和套接字后退出,服务器在检测到连接关闭后,关闭套接字自动结束。
我们的实例是unix下基于bsd socket的服务器程序和windows下基于winsock的客户机程序之间的通信。服务器在主机unix下直接运行,前台和后台均可;客户机在windows下运行,带一个参数,即主机的名字。如win client rs6000,rs6000是在hosts文件中已定义好的主机名。
我们先看客户机程序,首先定义几个宏、菜单资源和部分全局变量。
程序1:部分windows程序源代码(宏、菜单和变量)
#define userport 3333/*用户定义端口号*/
#define idm-start101/*“启动”菜单项标志*/
#define idm-exit102/*“退出”菜单项标志*/
#define um-sockwm-user+0x100/*用户定义网络消息*/
clientmenu menu/*客户机菜单*/
begin
popup "&server"
begin
menuitem "&start...", idm-start
menuitem "&stop",idm-stop
end
end
#include <winsock.h>/*必须包含winsock.h头文件*/
handlehinst;
charserver-address = {0};/*服务器地址缓冲区*/
charbuffer;/*接收发送缓冲区*/
char far *lpbuffer=&buffer;
sockets=0;/*套接字*/
struct sockaddr-in dst-addr;/*目标地址*/
struct hostent *hostaddr;/*主机地址*/
struct hostent hostnm;
intcount=0;/*发送接收循环计数器*/
客户机程序的窗口主函数很简单,它在注册窗口类、建立窗口后,只是给主窗口函数发送一个用户消息,然后就进入windows消息处理循环。
程序2:部分windows程序源代码(窗口主函数)
int pascal winmain(handle hinstance, handle hprevinstance, lpstr lp
cmdl
ine, int ncmdshow)
{
hwnd hwnd;
msgmsg;
lstrcpy((lpstr) server-address, lpcmdline);/*取主机名字*/
if (!hprevinstance)
if (!initapplication(hinstance))
return (false);
hinst=hinstance;
hwnd=createwindow("clientclass","windows echo client",
ws-overlappedwindow,cw-usedefault,cw-usedefault,
cw-usedefault,cw-usedefault,
null,null,hinstance,null);
if (!hwnd)
return (false);
showwindow(hwnd,ncmdshow);
updatewindow(hwnd);
/*给主窗口函数发送wm-user消息*/
postmessage(hwnd,wm-user,(wparam) 0,(lparam) 0);
while (getmessage(&msg,null,null,null)) {
translatemessage(&msg);
dispatchmessage(&msg);
}
return (msg.wparam);
}
主窗口函数clientproc是程序的主要部分,它处理相关的消息:在接到消息wm-user后,它调用函数wsastartup()初始化windows sockets dll,并检查其版本号,然后通过主机名获取主机地址;在接到消息wm-command时,如果是命令idm-start,则调用子程序client()建立套接字,并试图和服务器建立连接,如果是命令idm-stop,则调用函数wsacleanup()终止windows sockets dll,并发出终止应用程序的消息;在接到消息um-sock时,它根据参数lparam指示的网络事件,进行相应的操作,然后选择下一个期望的网络事件。
程序3:部分windows程序源代码(主窗口函数)
long far pascal
clientproc(hwnd hwnd, unsigned message, uint wparam, long lparam)
{
int length,i;
wsadatawsadata;/*描述windows sockets实现细节的数据结构*/
intstatus;
switch (message) {
case wm-user:
status=wsastartup(0x101,&wsadata);
if (status !=0) {
alertuser(hwnd,"wsastartup() failed");
postquitmessage(0);
}
if (lobyte(wsadata.wversion) !=1 || hibyte(wsadata.wversion) !=1)
{ /*现在支持的版本是winsock.dll 1.1*/
alertuser(hwnd, "wsastartup() version not match");
wsacleanup();
postquitmessage(0);
}
hostaddr=gethostbyname(server-address);
if (hostaddr==null) {
alertuser(hwnd, "gethostbyname error ");
wsacleanup ();
postquitmessage(0);
}
memcpy(&hostnm,hostaddr,sizeof(struct hostent));
break;
case wm-command:
switch (wparam) {
case idm-start:
if (!client(hwnd)) {
closesocket(s);
alertuser(hwnd, "start failed");
}
break;
case idm-stop:
wsacleanup();
postquitmessage(0);
break;
}
break;
case um-sock:
switch (lparam) {
case fd-connect:/*事件:连接建立*/
if (!set-select(hwnd, fd-write))/*选择:期望发送*/
closesocket(s);
break;
case fd-read:/*网络事件:读准备好*/
if (!receive-pkt(hwnd)) {/*接收数据*/
alertuser(hwnd, "receive packet failed");
closesocket(s);
break;
}
if (!set-select(hwnd, fd-write))/*选择:期望发送*/
closesocket(s);
break;
case fd-write:/*网络事件:写准备好*/
for (i=0;i<1024;i++)
buffer=(char) 'a'+i % 26;
length=1024;
if (!(send-pkt(hwnd,length))) {/*发送数据*/
alertuser(hwnd, "packet send failed");
closesocket(s);
break;
}
if (!set-select(hwnd, fd-read)) /*选择:期望接收*/
closesocket(s);
break;
case fd-close:/*网络事件:连接关闭。操作:停止异步选择*/
if (wsaasyncselect(s,hwnd,0,0)==socket-error)
alertuser(hwnd, "wsaasyncselect failed");
alertuser(hwnd, "socket has been closed");
break;
default:/*网络出错则警告,其他事件忽略*/
if (wsagetselecterror(1param) !=0) {
alertuser(hwnd, "socket report failure");
closesocket(s);
break;
}
break;
}
break;
case wm-destroy:
closesocket(s);/*关闭窗口前应该关闭套接字,并*/
wsacleanup();/*终止windows sockets dll*/
postquitmessage(0);
break;
default:
return (defwindowproc(hwnd, message,
wparam, lparam));
}
return (null);
}
程序4:部分windows程序源代码(子程序)
bool client(hwnd hwnd)/*客户机子程序*/
{
if (!make-skt(hwnd))/*建立套接字*/
return(false);
if (!set-select(hwnd,fd-connect))/*设置异步连接*/
return(false);
if (!connect-skt(hwnd))/*建立连接*/
return(false);
return(true);
}
bool receive-pkt(hwnd hwnd)/*接收数据子程序*/
{
hdc dc;
intlength;
int11,12,13;
charlinel,line2,line3;
count ++;/*循环计数器加1*/
if ((length=recv(s,lpbuffer,1024,0))==socket-error)
return(false); /*如果接收数据出错,则返回false*/
if (length==0) /*接收数据长度为零,表示连接中断*/
return(false);
if (dc=getdc(hwnd)) { /*接收数据成功,显示信息*/
11=wsprintf((lpstr) line1,"tcp echo client no.%d",count);
12=wsprintf((lpstr) line2,"received %d bytes", length);
13=wsprintf((lpstr) line3,"those are:%c,%c,%c,%c,%c,%c",
buffer,buffer,buffer,buffer,buffer,buffer
);
textout(dc, 10, 2, (lpstr) linel, 11);
textout(dc, 10, 22, (lpstr) line2, 12);
textout(dc, 10, 42, (lpstr) line3, 13);
releasedc(hwnd, dc);
}
return(true);
}
bool set-select(hwnd hwnd, long levent)/*异步选择子程序*/
{
if (wsaasyncselect(s,hwnd, um-sock, levent)==socket-error) {
alertuser(hwnd, "wsaasyncselect failed");
return (false);
}
return (true);
}
bool make-skt(hwnd hwnd)/*建立套接字子程序*/
{
if ((s=socket(af-inet,sock-type,0))==invalid-socket) {
alertuser(hwnd, "socket failed");
return (false);
}
return (true);
}
bool connect-skt(hwnd hwnd)/*建立连接子程序*/
{
memset((void*) &dst-addr, sizeof(dst-addr),0);
dst-addr.sin-family=af-inet;
dst-addr.sin-port=htons(userport);
dst-addr.sin-addr.s-addr=*((unsigned long *)hostnm.h-addr-list );
if (connect(s, (struct sockaddr *) & dst-addr,
sizeof(dst-addr))==socket-error) {
alertuser(hwnd, "connect failed");
return (false);
}
return (true);
}
bool send-pkt(hwnd hwnd, int len)/*发送数据子程序*/
{
int length;
if ((length=send(s,lpbuffer,len,0))==socket-error)
return (false);
else if (length !=len) {
alertuser(hwnd, "send length not match!");
return(false);
}
return (true);
}
我们用最简单的语句编制一个unix下基于bsd socket的服务器程序,它在建立连接后,只负责将收到的数据发回去,在连接断开后,服务器关闭套接字返回。要编制在windows下的服务器程序,可参照客户机程序,使用winsock的异步选择机制。
程序5:unix下服务器程序源代码
/*tcp/ip必要的头文件*/
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define userport 3333/*用户定义端口号,与客户机相同*/
#define host-ip-addr "166.111.8.80"/*我们的主机地址*/
main(int argc, char **argv)
{
char buf;/*buffer for sending and receiving data*/
struct sockaddr-in client;/*client address information*/
struct sockaddr-in server;/*server address information*/
int s;/*socket for accepting connections*/
int ns;/*socket connected to client*/
int namelen;/*length of client name*/
int pktlen;/*length of packet received or sended*/
if ((s = socket(af-inet,sock-stream, 0))<0) {
perror("socket()");
return;
}
/*bind the socket to the server address.*/
bzero((char*)&server, sizeof(server));
server.sin-len =sizeof(struct sockaddr-in);
server.sin-family=af-inet;
server.sin-port =htons(userport);
server.sin-addr.s-addr=inaddr-any;
if (bind(s, (struct sockaddr *)&server, sizeof(server)) <0) {
perror ("bind()");
return;
}
/* for connections. specify the backlog as 1. */
if (listen(s,1)!=0) {
perror("listen()");
return;
}
/*accept a connection.*/
namelen=sizeof(client);
if ((ns = accept(s, (struct sockaddr *)&client,&namelen))==-1) {
perror("accept()");
return;
}
/*receive the message on the newly connected socket.*/
for (;;){
if ((pktlen = recv(ns, buf, 1024, 0))<0) {
perror("recv()");
break;
}
else if (pktlen==0) {
printf("recv():return failed,connection is shut down! ");
break;
}
else printf("recv():return success, packet length = %d ",pktlen);
sleep(1); /*sleep() 1秒钟是为了减慢数据交换速度*/
/*send the message back to the client.*/
if (send(ns, buf, pktlen, 0) <0) {
perror("send ()");
break;
}
else printf("send():return success, packet length = %d ",pktlen);
}
close(ns);
close(s);
printf("server ended successfully");
}
四、结束语
本文试图通过一个实例来说明如何使用winsock实现windows下网络实时通信。从上面的讨论可以看出,使用winsock编制windows下网络软件是比较方便的,winsock提供的异步选择机制使socket强大的网络编程功能能够在windows下得到应用。相信随着internet的推广,tcp/ip网络协议的广泛使用,使用winsock编制windows网络实时通信软件将会有一个大的。
1 martin hall等.windows sockets-an open interface for network programm ing under microsoft windows.winsock document, 1993(6).2 孙义等.unix环境下的网络程序设计.北京:希望公司,1991.3 梁振军等.新编tcp/ip协议与机网络互联技术.北京:希望公司,1992.