基于UDP协议采用Winsock构建网络聊天室.docx
《基于UDP协议采用Winsock构建网络聊天室.docx》由会员分享,可在线阅读,更多相关《基于UDP协议采用Winsock构建网络聊天室.docx(12页珍藏版)》请在冰豆网上搜索。
基于UDP协议采用Winsock构建网络聊天室
摘要
基本的网络编程都是建立在Winsock基础上的,作为网络编程接口,Winsock屏蔽了网络底层的复杂的协议和数据结构,因此,在Win32平台上,访问众多的基层网络协议,Winsock是首选接口。
用Winsock构建一个网络聊天室,有两种基本的方式:
数据报方式和流方式,即面向无连接的数据报方式和面向连接的流方式,对应的协议分别为UDP(UserDatagramProtocol)协议和TCP(TransportControlProtocol))协议。
本次我们设计的网络聊天室是基于UDP协议的无连接方式,软件编程则主要使用Winsock提供的API函数。
关键字:
网络编程UDP协议API函数
1设计目的与功能要求
1.1设计目的
(1)编写一个简单的C/S模式的网络聊天室软件
(2)进一步掌握利用VisualC++进行程序设计的能力;
(3)进一步理解和运用面向对象程序设计的思想和方法;
(4)初步掌握开发一个小型实用系统的基本方法;
(5)理解Windows程序的运行过程。
1.2网络聊天室功能要求
网络聊天室系统设计完成后,我们可以在一台计算机上运行服务端程序,然后在另一网络的其他计算机上运行客户端程序,登录到服务器上,各个客户之间就可以聊天,或者直接在一台计算机上同时运行服务器端和客户端,然后两者之间进行通信。
1.2.1服务器端功能要求
(1)初始化socket,创建服务器端。
(2)维护一个链表,保存所有用户的IP地址、端口信息。
(3)接受用户传送来的聊天信息,然后向链表中的所用用户转发。
(4)接受用户传送来的连接判断命令,并向用户发出响应命令。
1.2.2客户端功能要求
客户端界面上的两个文本框,一个用于显示接受的聊天信息,一个用来接受用户输入的聊天信息。
当按下“发送”按钮时将信息发送给服务器。
2设计原理
本次设计的题目是实现一个简单的C/S模式的网络聊天室,基于UDP协议,是不可靠的面向无连接的方式。
设计的基本原理也就是UDP编程原理。
UDP协议是一个简单的面向数据报的传输层协议,又叫用户数据报协议。
它提供了无连接的、不可靠的数据传输服务。
无连接是指它不像TCP协议那样在通信前先于对方建立连接以确定对方的状态。
不可靠是指它直接安装指定的IP地址和端口号将数据包发送出去,如果对方不在线的话数据就可能丢失。
UDP协议编程原理如下:
2.1服务器端原理
(1)创建套节字(socket);
(2)绑定IP地址和端口(bind);
(3)收发数据(sendto/recvfrom);
(4)关闭连接(closesocket)。
2.2客户端原理
(1)创建套节字(socket);
(2)收发数据(sendto/recvfrom);
(3)关闭连接(closesocket)。
UDP协议用于发送和接收数据的函数是sendto和recvfrom。
它们的原形如下:
intsendto(
SOCKETs,//用于发送数据的套节字
ConstcharFAR*buf,//指向发送数据的缓冲区
intlen,//要发送数据的长度
intflags,//一般指定为0
//指向一个包含目标地址和端口号的sockaddr_in结构
conststructsockaddr*to,
inttolen);//为socket_in的结构大小
同样UDP协议接收数据也需要知道通信对端的地址信息。
intrecvfrom(SOCKETs,charFAR*buf,intlen,intflags,structsockaddrFAR*from,intFAR*fromlen);
2.3网络聊天室设计流程
该程序设计使用WindowsSockets编程。
在数据传输协议中,我们使用UDP协议,UDP协议是无连接的协议,在服务器端不调用listen函数进行监听,也不调用accept函数建立连接,其操作流程图如下图3-2所示:
首先,用socket函数建立套接字,然后根据本地的IP进行绑定,然后就可以通过使用send函数和reve函数来进行数据的发送和接收了。
利用C语言编写Windows应用程序有两种方式:
一种是WindowsC编程方式,另一种是VisualC++编程方式。
在一般情况下,VisualC++编程方式编写的程序源代码量小、开发时的工作量小、工作难度也较小,但编译后的代码量较大,运行速度略低;而W
indowsC编程方式编写的程序源代码量虽然较大,但可执行代码效率高。
随着技术的进步,VisualC++编程方式已被广泛采用,但象网络编程等一些对速度要求高、对硬件操作较多的程序,大多数还是用WindowsC编程方式开发的。
另外,学习WindowsC程序设计,还有助于更深入地了解Windows的内幕和WindowsAPI。
3实现过程设计
数据报方式又称无连接方式,对应的是UDP(UserDatagramProtocol)协议。
这种方式不提供数据无错保证,数据可能丢失或重复并且接收顺序混乱,后发出的报文可能会先收到,并且报文的长度是有限制的;不过,由于取消了重发校验机制,能够达到较高的通信速率,可以用于对数据可靠性要求不高的通信,如实时的语音、图像传送和广播消息等。
和C语言一样,函数是WindowsC编程的最基本的单位。
不过,WindowsC主要使用API函数,而网络编程则主要使用Winsock提供的API函数。
因此,在该网络聊天室的设计中,我们将采用Winsock编程,而Winsock编程的一般过程是比较固定的。
3.1服务器端程序设计
3.1.1Winsock库德装入、初始化和释放
所以的Winsock函数都是从WS2_32.DLL库导出的,VC++在默认的情况下并没有连接到该库,如果想使用WinsockAPI,就必须包含相应的库文件。
#pragmacomment(lib,“wsock32.lib”)
每一个使用winsock的应用程序,都必须进行WSAStart函数调用,并且只有在调用成功之后才能使用其它的winsock网络操作函数。
WSAstartup必须是应用程序首先调用的Winsock函数。
它允许应用程序指定所需的WindowsSocketsAPI的版本,获取特定Winsock实现的详细信息。
仅当这个函数成功执行之后,应用程序才能调用其他的WinsockAPI函数。
WSAStartup()函数原形为:
intWSAStartup(
//应用程序支持的最高Winsock库版本,高字节为此版本号,低字节为主版本号
WORDwVresionRequested
//一个指向WSADATA结构的指针,它用来返回DLL库德详细信息。
LPWSADATAlpWSAData);
每一个对WSAStartup的调用必须对应一个对WSAClearnup的调用,这个函数释放Winsock库。
intWSAClearnup(void);
该过程的核心程序代码如下:
WORDVersionRequested=MAKEWORD(1,1);
WSADATAwsaData;
WSAStartup(VersionRequested,&wsaData);//启动winsock服务
if(wsaData.wVersion!
=VersionRequested)
{
MessageBox(NULL,"FirstFailed!
","Error",0);
WSACleanup();
}
3.1.2套节字的创建和关闭
Winsock网络通信的第一步通常就是调用socket()函数。
所有的通信在建立之前都有要创建一个Socket。
该函数的功能与文件操作中的fopen()类似,返回值是由Winsock定义的一种数据类型SOCKET,它实际是一个整型数据,是Socket创建成功时,Windows分配给程序的Socket编号,后面调用传输函数时,可以把它像文件指针样引用。
使用套节字之前,必须调用socket函数创建一个套节字对象,此函数调用成功将返回套节字的句柄。
Socket()函数的原形如下:
SOCKETsocket(
intaf,//用来指定套节字使用的地址格式,WinSock中只支持AF_INET
inttype,//用来指定套节字的类型
//配合type参数使用,用来指定使用的协议类型,可以是IPPROTO_TCP等
intprotocol);
type参数用来指定套节字的类型。
套节字有流套节字、数据报套节字和原始套节字等,下面是常见的几种套节字类型定义:
SOCK_STREAM流套节字,使用TCP协议提供有连接的可靠的传说
SOCK_DGRAM数据报套节字,使用UDP协议提供无连接的不可靠传输
SOCK_RAW原始套节字,Winsock接口并不使用某种特定的协议去封装它,而是有程序的自行处理数据报以及协议首部。
当type参数指定为SOCK_STREAM和SOCK_DGRAM时,系统已经明确确定使用UDP协议来工作,所以protocol参数可以指定为0。
函数执行失败返回INVYLID_SOCKET(即-1),可以通过调用WSAGetLasError取得错误代码。
当不使用socket创建的套节字时,应该调用closesocket函数将它关闭。
如果没有错误发生,函数返回0,否则返回SOCKET_ERROR。
函数用法如下:
intclosesocket(SOCKETs);//函数唯一的参数就是关闭的套节字的句柄
该过程的核心程序代码如下:
if(ServerSock!
=0)
{
closesocket(ServerSock);//关闭套接字
ServerSock=0;
}
if(ServerSock==0)
{
//创建套接字对象,数据报套接字,SOCK_STREAM为流套接字
//建立套接字失败if((ServerSock=socket(AF_INET,SOCK_DGRAM,0))==SOCKET_ERROR)
MessageBox(NULL,"SetSocketFailed!
","Error",0);
}
3.1.3绑定套节字到指定的IP地址和端口号
成功创建了Socket之后,就应该选定通信的对象。
首先是自己的程序要与网上的哪台计算机通话;其次,在多任务的系统下,该台计算机上可能会有几个程序在工作,必须指出要与哪个程序通信。
前者可以通过IP地址来确定,而后者则由端口号来确定的。
为套节字关联本地地址的函数是bind,函数原形如下:
intbind(
SOCKETs,//套节字句柄
conststructsockaddr*name,//要关联的本地地址
intnamelen);//地址的长度
bind函数用在没有建立连接的套节字上,它的作用是绑定面向连接的或者无连接的套节字。
当一个套节字被socket函数创建之后,它存在于指定的地址家族里,但是它是名命名的。
bind函数通过安排一个本地名称到未命名的socket建立此socket的本地关联。
本地名称包含3个部分:
主机地址、协议号和端口号。
将套节字与IP地址和端口号绑定的核心程序代码:
ServerAddr.sin_family=AF_INET;
//系统自动使用当前主机配置的所有IP地址
ServerAddr.sin_addr.S_un.S_addr=INADDR_ANY;
serPort=GetDlgItemInt(hDlg,IDC_Sport,&serErr,TRUE);
ServerAddr.sin_port=htons((u_short)serPort);
//bind()绑定套接字在指定的端口
if(bind(ServerSock,(LPSOCKADDR)&ServerAddr,sizeof(ServerAddr))==SOCKET_ERROR)
MessageBox(NULL,"BindPortFailed!
","Error",0);
return1;
3.1.4设置套节字进入监听状态
listen函数置套节字进入监听状态。
intlisten(
SOCKETs,//套节字句柄
intbacklog);//监听队列中允许保持的尚未处理的最大连接数量
为了接受连接,首先使用socket函数创建一个套节字,然后使用bind函数绑定它到一个本地地址,再使用listen函数为到达的连接指定一个backlog,最后使用accept接受请求的连接。
listen仅应用在支持连接的套节字上,如SOCK_STREAM类型。
函数成功执行后,套节字s进入了被动模式,到来的连接会被通知,排队等接受处理。
在同一时间处理对个连接请求的服务器通常使用listen函数:
如果一个连接请求到达,并且排队已满,客户端将接收WSAECONNREFUSED错误。
在基于UDP协议的网络聊天室设计中,不需要listen函数,而这里的listen监听知识绑定一个端口。
其核心代码如下:
ServerSocket=socket(AF_INET,SOCK_DGRAM,0);
if(ServerSocket==INVALID_SOCKET)
{
MessageBox(NULL,”socket创建失败”,”Error”,0);
returnfalse;
}
srv.sin_family=AF_INET;
srv.sin_addr.s_addr=htonl(INADDR_ANY);//任何地址
srv.sin_port=htons(PortNum);
if(bind(ServerSocket,(structsockaddr*)&srv,sizeof(srv))!
=0)
{
MessageBox(NULL,”socket绑定端口失败”,”Error”,0);
closesocket(ListenSocket);
returnfalse;
}
3.1.5收发数据
对数据报套节字来说,一般使用sendto和recvfrom函数来收发数据。
在这次网络聊天室的设计中,我们采用服务器只接收数据而不进行转发数据,从而简化了部分任务,不过其核心功能还是实现的很好。
recvfrom()/sendto()的函数原型如下:
intrecvfrom(SOCKETs,charFAR*buf,intlen,intflags,structsockaddrFAR*from,intFAR*fromlen);
intsendto(SOCKETs,constcharFAR*buf,intlen,intflags,conststructsockaddrFAR*to,inttolen);
使用说明:
s<输入>:
是连接用的socket。
buf、len<输入>:
发送或接收的数据包字符串的地址和长度。
flags<输入>:
一般取0。
from、fromlen/to、tolen<输入>:
含义和用法与bind()中的相同,分别表示接收和发送数据的对象。
实现该功能的核心程序代码入下:
caseIDC_receive:
//接受按钮的ID号
//接受数据的对象
recvfrom(ServerSock,Output,256,0,(LPSOCKADDR)&ServerAddr,&fromlen);
SetDlgItemText(hDlg,IDC_Sedit1,Output);
return1;
3.1.6关闭连接
这个模块主要实现的功能包括关闭由socket创建的数据报套节字、释放Winsock库和清除一个模态对话框,并使系统中止对对话框的任何处理。
实现这几个功能的函数为:
closesocket()、WSACleanup()和EndDialog()函数。
实现这些功能的程序代码如下:
caseIDC_closeserver:
closesocket(ServerSock);
WSACleanup();//释放Winsock库
//清除一个模态对话框,并使系统中止对对话框的任何处理
EndDialog(hDlg,TRUE);
return1;
3.2客户端程序设计
客户端程序的设计思路和服务器端有很大的相似之处,只不过客户端不需要绑定IP地址和端口号,也不需要进行监听功能。
所以,在总体设计上,比服务器端稍微简单一点,与服务器的实现原理是相同的。
客户端程序的设计中也要由socket创建数据报套节字,还要用sendto()发送数据,最后要关闭套节字,释放Winsock库。
在客户端,数据方式(UDP协议)构建网络聊天室程序设计中,Winsock库德载入和初始化,创建套节字等功能和服务器端的完全相同,在这里就不再重复。
不同之处在与客户端要发送数据而不进行接收数据。
3.2.1发送数据程序设计
【函数原型】
intsendto(SOCKETs,constcharFAR*buf,intlen,intflags,conststructsockaddrFAR*to,inttolen);
【使用说明】
s<输入>:
是连接用的socket。
buf、len<输入>:
发送或接收的数据包字符串的地址和长度。
flags<输入>:
一般取0。
from、fromlen/to、tolen<输入>:
含义和用法与bind()中的相同,分别表示接收和发送数据的对象。
实现发送数据的核心程序代码如下:
caseIDC_send:
//发送按钮的ID号
ClientAddr.sin_family=AF_INET;
ClientAddr.sin_addr.S_un.S_addr=inet_addr(IPStr);
ClientPort=GetDlgItemInt(hDlg,IDC_Eport,&bErr,TRUE);
ClientAddr.sin_port=htons((u_short)ClientPort);
//使用指定的SocketFlags,将指定字节数的数据发送到指定的终结点sendto(ClientSocket,InputText,256,0,(LPSOCKADDR)&ClientAddr,sizeof(ClientAddr));
break;
3.2.2几个关键结构体的介绍
在整个基于UDP协议的网络聊天室程序设计中,我们一直都在使用几个比较重要的系统自定义的结构体,我们正是在这些结构体的基础上进行程序设计,并最终完成网络聊天室的设计。
在这里主要介绍Sockaddr_in结构体和IP地址sin_addr结构体。
Sockaddr_in的定义如下:
structsockaddr_in
{
shortsin_family;
unsignedshortsin_port;
struct in_addrsin_addr;
charsin_zero[8];
};
其中,sin_family是指一套地址族,它指定所要使用的通信协议,通常设为AF_INET;sin_port端口号;sin_addr是IP地址;而sin_zero[8]的作用,只是使该结构的大小和SOCKADDR结构大小相同。
IP地址sin_addr结构定义如下:
structin_addr
{
union
{
struct{u_chars_b1,s_b2,s_b3,s_b4;}S_un_b;
struct{u_shorts_w1,s_w2;}S_un_w;
u_longS_addr;
}S_un;
};
这样,对于一个IP地址,例如“192.168.0.1”,就可以用以下三种方法赋给一个sockaddr结构体(例如structsockaddr_inm_addr;):
方法1:
m_addr.sin_addr.S_un.S_un_b.s_b1=192;
m_addr.sin_addr.S_un.S_un_b.s_b2=168;
m_addr.sin_addr.S_un.S_un_b.s_b3=0;
m_addr.sin_addr.S_un.S_un_b.s_b4=1;
方法2:
m_addr.sin_addr.S_un.S_un_w.s_w1=(168<<8)|192;
m_addr.sin_addr.S_un.S_un_w.s_w2=(1<<8)|0;
方法3:
m_addr.sin_addr.S_un.S_addr=(1<<24)|(0<<16)|(168<<8)|192;
为了更方便地赋值,winsock还为我们提供了一个函数inet_addr(),可以把用字符串表示的IP地址“192.168.0.1”直接赋给结构体m_addr:
char*IP_String=”192.168.0.1”;
m_addr.sin_addr.S_un.S_addr=inet_addr(IP_String);
4程序界面设计
4.1服务器端界面设计
在服务器界面中,主要有一个接收数据的窗口,另外还需要建立服务器的按钮、建立服务器的端口号窗口、关闭服务器的按钮。
设计服务器界面是在VistualC++6.0中进行的,关键之处在于各个窗口或者按钮的ID号必须与程序中的ID号对应。
设计完成后,服务器端的界面入下:
图4-1服务器端界面
4.2客户端界面设计
在客户端界面中,主要有一个发送消息的窗口,另外还包括发送按钮、连接服务器的端口号、本地计算机的IP地址和关闭客户端的按钮。
设计客户端的界面基本过程和思路同设计服务器端的相同,开发工具都是VistualC++6.0。
客户端的界面如下:
图4-2客户端界面
5运行结果分析
网络聊天室设计的预期目的是能够在同一台计算机或者不同的计算机上进行通信。
也就是说,当客户端向服务器发送信息,当服务器端按下接收按钮时,客户端所发送的数据能够被服务器接收到。
而客户端只负责发送数据,不考虑所发数据是否能够可靠的全部被服务器接收。
客户端发送一条消息,服务器就接收一条消息时,服务器端与客户端的运行界面如下所示:
客户端发送多条消息,而服务器并没有全部接收时,服务器端与客户端的运行界面如下所示:
不过当服务器按下接收按钮时,有能够接收到全部数据。
6总结
在实验中,我们可以把这学期所学的理论知识和实践联系起来,在所要设计的程序中渐渐融会贯通。
虽然我们对这些知识还运用得还不是很熟练,但是相信在现在和今后的学习中会得到更加深刻的掌握。
本课程设计是设计一个程序,实现聊天功能,要求有客户端和服务器端。
通过设计我们进一步掌握利用VisualC++进行程序设计的能力;进一步理解和运用面向对象程序设计的思想和方法;初步掌握开发一个小型实用系统的基本方法;学会调试一个较长程序的基本方法。
总的来说通过本次实验,我学习到了许多东西,增强了