计 算 机 网 络
课程设计指导书
(11级计算机、网络工程专业)
湖南科技大学计算机科学与工程学院
20##年1月
一、课程设计目的
1.加深对计算机网络通信系统的工作原理的理解
通过编写计算机程序实现、模拟网络的某些功能,使学生理解并掌握计算机网络的基本工作原理及工作过程。
2.实现应用进程跨越网络的通信
了解系统调用和应用编程接口基本知识,理解应用程序和操作系统之间传递控制权的机制,掌握套接字的创建和运用,通过socket系统调用实现跨网通信。
3.提高网络编程和应用的能力
提高实际编程能力和灵活运用所学知识解决问题的能力。培养调查研究、查阅技术文献、资料、手册以及编写技术文档的能力,理论应用于实践的能力。
二、课程设计要求
1. 仔细分析每一个实验的具体内容、步骤和要求,按设计要求完成任务。
2. 使用C和C++语言,用基础SOCKET编程方法实现设计功能。
3. 程序设计需要完整的程序流程图、说明文档和源程序清单,设计者需要清楚每个模块的功能和原理。
4. 完成程序的编写、编译、执行和测试,每人至少完成三个题目的设计工作。
5. 提交课程设计报告(含括课程设计名称、课程设计题目、课程设计内容、课程设计步骤、调试过程、课程设计结果及结果分析、心得体会)、程序源文件、可执行文件各一份。
三、课程设计考核方式
1. 测试中演示所设计的程序,占总成绩30%;
2. 测试中回答指导老师所提出的问题,占总成绩20%;
3. 设计报告,占总成绩40%;
4. 考勤情况,占总成绩10%。
三、课程设计题目
1、网络聊天程序的设计与实现
参照附录1,了解Socket通信的原理,在此基础上编写一个聊天程序。
2、Ping程序设计与实现
参照附录2,了解Ping程序的实现原理,并调试通过。如有可能则在此基础上,编写一个可以测试本局域网的所有机器是否在线的程序。如图1所示的QuickPing程序。
图1 QuickPing运行界面效果图
3、基于IP多播的网络会议程序
参照附录3的局域网IP多播程序,设计一个图形界面的网络会议程序(实现文本多播方式即可)。
4、网络嗅探器的设计与实现
参照附录4 raw socket编程例子,设计一个可以监视网络的状态、数据流动情况以及网络上传输的信息的网络嗅探器。
5、电子邮件客户端程序设计与实现
参照教材6.5节原理,设计一个电子邮件客户端程序。
6、TELNET终端设计与实现
参照RFC854、RFC855文档,设计一个TELNET终端程序。
7、网络代理服务器的设计与实现
实现一个简易的proxy程序。proxy程序的功能:能够做“二传手”的工作。它自身处在能同时连通外界目标服务器和我的机器的位置上。我的机器把请求发送给它,它接受请求,把请求原封不动的抄下来发送给外界目标服务器;外界目标服务器响应了请求,把回答发送给它,它再接受回答,把回答原封不动的抄下来发送给我的机器。这样,我的机器实际上是把它当作了目标服务器(由于是原封不动的转抄,请求和回答没有被修改)。而它则是外界目标服务器的客户端。
四、推荐参考文献
[1] 谢希仁,计算机网络(第五版),电子工业出版社,2008.
[2] Andrew S.Tanenbaum.计算机网络(第四版)[M].北京:清华大学出版社,2004.
[3] 中国Linux论坛:http://www.linuxforum.net/
[4] UNIX技术网站(永远的UNIX):http://fanqiang.chinaunix.net/
[5] Google http://www.google.cn/
[6] 蒋清明,C语言程序设计,人民邮电出版社,2008.
[7] 陈维兴,林小茶, C++面向对象程序设计(第二版), 中国铁道出版社, 20##年12月
五、附录
附录1:Windows Socket编程简介
附录2:PING源代码
附录3:用Visual C++实现局域网IP多播
附录4:raw socket编程例子(基于LINUX操作系统)
附录5:课程设计报告封面设计
附录1、Windows Socket编程简介
使用WinSock API的编程,应该了解TCP/IP的基础知识。虽然你可以直接使用WinSock API来写网络应用程序,但是,要写出优秀的网络应用程序,还是必须对TCP/IP协议有一些了解的。
1. TCP/IP协议与WinSock网络编程接口的关系
WinSock 并不是一种网络协议,它只是一个网络编程接口,也就是说,它不是协议,但是它可以访问很多种网络协议,你可以把它当作一些协议的封装。现在的 WinSock已经基本上实现了与协议无关。你可以使用WinSock来调用多种协议的功能。那么,WinSock和TCP/IP协议到底是什么关系呢?实际上,WinSock就是TCP/IP协议的一种封装,你可以通过调用WinSock的接口函数来调用TCP/IP的各种功能.例如我想用TCP/IP 协议发送数据,你就可以使用WinSock的接口函数Send()来调用TCP/IP的发送数据功能,至于具体怎么发送数据,WinSock已经帮你封装好了这种功能。
2、TCP/IP协议介绍
TCP/IP协议包含的范围非常的广,它是一种四层协议,包含了各种硬件、软件需求的定义。TCP/IP协议确切的说法应该是TCP/UDP/IP协议。UDP协议(User Datagram Protocol 用户数据报协议),是一种保护消息边界的,不保障可靠数据的传输。TCP协议(Transmission Control Protocol 传输控制协议),是一种流传输的协议。他提供可靠的、有序的、双向的、面向连接的传输。
保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。
而面向流则是指无保护消息边界的,如果发送端连续发送数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。
举例来说,假如,我们连续发送三个数据包,大小分别是2k、4k、8k,这三个数据包都已经到达了接收端的网络堆栈中,如果使用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有三次接收动作,才能够把所有的数据包接收完。而使用TCP协议,我们只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作。
这就是因为UDP协议的保护消息边界使得每一个消息都是独立的。而流传输,却把数据当作一串数据流,它不认为数据是一个一个的消息。所以有很多人在使用TCP协议通讯的时候,并不清楚TCP是基于流的传输,当连续发送数据的时候,他们时常会认识TCP会丢包。其实不然,因为当它们使用的缓冲区足够大时,它们有可能会一次接收到两个甚至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个数据包,而已经接收的其它据包却被忽略了。
3.WinSock编程简单流程
WinSock编程分为服务器端和客户端两部分,TCP服务器端的大体流程如下:
对于任何基于WinSock的编程首先必须要初始化WinSock DLL库。
int WSAStarup( WORD wVersionRequested,LPWSADATA lpWsAData )。
wVersionRequested是我们要求使用的WinSock的版本。
调用这个接口函数可以初始化WinSock 。
然后必须创建一个套接字(Socket)。
SOCKET Socket(int af,int type,int protocol);
套接字可以说是WinSock通讯的核心。WinSock通讯的所有数据传输,都是通过套接字来完成的,套接字包含了两个信息,一个是IP地址,一个是Port端口号,使用这两个信息,就可以确定网络中的任何一个通讯节点。
当调用了Socket()接口函数创建了一个套接字后,必须把套接字与你需要进行通讯的地址建立联系,可以通过绑定函数bind来实现这种联系。
int bind(SOCKET s,const struct sockaddr FAR* name,int namelen) ;
struct sockaddr_in{
short sin_family ;
u_short sin_port;
struct in_addr sin_addr ;
char sin_sero[8] ;
}
就包含了需要建立连接的本地的地址,包括地址族、IP和端口信息。sin_family字段必须把它设为AF_INET,这是告诉WinSock使用的是IP地址族。sin_port就是要用来通讯的端口号。sin_addr就是要用来通讯的IP地址信息。
在这里,必须还得提一下有关'大头(big-endian)'小头(little-endian)'。因为各种不同的计算机处理数据时的方法是不一样的,Intel X86处理器上是用'小头'形式来表示多字节的编号,就是把低字节放在前面,把高字节放在后面,而互联网标准却正好相反,所以,必须把主机字节转换成网络字节的顺序。WinSock API提供了几个函数。
把主机字节转化成网络字节的函数;
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
把网络字节转化成主机字节的函数;
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort) ;
这样,设置IP地址和port端口时,就必须把主机字节转化成网络字节后,才能用Bind()函数来绑定套接字和地址。
当绑定完成之后,服务器端必须建立一个监听的队列来接收客户端的连接请求。
int listen(SOCKET s,int backlog);
这个函数可以把套接字转成监听模式。
如果客户端有了连接请求,我们还必须使用
int accept(SOCKET s,struct sockaddr FAR* addr,int FAR* addrlen);
来接受客户端的请求。
现在基本上已经完成了一个服务器的建立,而客户端的建立的流程则是初始化WinSock,然后创建Socket套接字,再使用
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen) ;
来连接服务端。
下面是一个最简单的创建服务器端和客户端的例子:
服务器端的创建:
WSADATA wsd;
SOCKET sListen;
SOCKET sclient;
UINT port = 800;
int iAddrSize;
struct sockaddr_in local , client;
WSAStartup( 0x11 , &wsd );
sListen = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP );
local.sin_family = AF_INET;
local.sin_addr = htonl( INADDR_ANY );
local.sin_port = htons( port );
bind( sListen , (struct sockaddr*)&local , sizeof( local ) );
listen( sListen , 5 );
sClient = accept( sListen , (struct sockaddr*)&client , &iAddrSize );
客户端的创建:
WSADATA wsd;
SOCKET sClient;
UINT port = 800;
char szIp[] = "127.0.0.1";
int iAddrSize;
struct sockaddr_in server;
WSAStartup( 0x11 , &wsd );
sClient = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP );
server.sin_family = AF_INET;
server.sin_addr = inet_addr( szIp );
server.sin_port = htons( port );
connect( sClient , (struct sockaddr*)&server , sizeof( server ) );
当服务器端和客户端建立连接以后,无论是客户端,还是服务器端都可以使用
int send( SOCKET s,const char FAR* buf,int len,int flags);
int recv( SOCKET s,char FAR* buf,int len,int flags);
函数来接收和发送数据,因为,TCP连接是双向的。
当要关闭通讯连接的时候,任何一方都可以调用
int shutdown(SOCKET s,int how);
来关闭套接字的指定功能,再调用
int closeSocket(SOCKET s) ;
来关闭套接字句柄,这样一个通讯过程就算完成了。
计算机网络(第5版)280页图6-32所示的系统调用使用顺序:
注意:上面的代码没有任何检查函数返回值,如果你作网络编程就一定要检查任何一个WinSock API函数的调用结果,因为很多时候函数调用并不一定成功。上面介绍的函数,返回值类型是int的话,如果函数调用失败的话,返回的都是SOCKET_ERROR。
4.VC中socket编程步骤
sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);基于TCP的socket编程是采用的流式套接字。在这个程序中,将两个工程添加到一个工作区。要链接一个ws2_32.lib的库文件。
服务器端编程的步骤:
1:加载套接字库,创建套接字(WSAStartup()/socket());
2:绑定套接字到一个IP地址和一个端口上(bind());
3:将套接字设置为监听模式等待连接请求(listen());
4:请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
5:用返回的套接字和客户端进行通信(send()/recv());
6:返回,等待另一连接请求;
7:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。
服务器端代码如下:
#include <stdio.h>
#include <Winsock2.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return;
}
if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) {
WSACleanup( );
return;
}
SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
listen(sockSrv,5);
SOCKADDR_IN addrClient;
int len=sizeof(SOCKADDR);
while(1){
SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);
char sendBuf[50];
sprintf(sendBuf,"Welcome %s to here!",inet_ntoa(addrClient.sin_addr));
send(sockConn,sendBuf,strlen(sendBuf)+1,0);
char recvBuf[50];
recv(sockConn,recvBuf,50,0);
printf("%s\n",recvBuf);
closesocket(sockConn);
}
}
客户端编程的步骤:
1:加载套接字库,创建套接字(WSAStartup()/socket());
2:向服务器发出连接请求(connect());
3:和服务器端进行通信(send()/recv());
4:关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。
客户端的代码如下:
#include <stdio.h>
#include <Winsock2.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return;
}
if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) {
WSACleanup( );
return;
}
SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
send(sockClient,"hello",strlen("hello")+1,0);
char recvBuf[50];
recv(sockClient,recvBuf,50,0);
printf("%s\n",recvBuf);
closesocket(sockClient);
WSACleanup();
}
附录2、PING源代码
// PING.H
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct tagIP_HEADER{
unsigned int h_len:4; //length of header (4b)
unsigned int ver:4; //version (4b)
unsigned char tos; //tos (1B)
unsigned short total_len; //total length (2B)
unsigned short ident; //identification (2B)
unsigned short frag_flags; //frag and flags (2B)
unsigned char ttl; //time of lives (1B)
unsigned char protocol; //protocol (1B)
unsigned short checksum; //checksum (2B)
unsigned int sourceip; //source ip (4B)
unsigned int destip; //destination ip (4B)
}IP_HEADER,*PIP_HEADER; //length of total (20B)
typedef struct tagIP_OPT_HEADER{
unsigned char code; //option type (1B)
unsigned char len; //length of option header (1B)
unsigned char ptr; //下一个可存放地址的位置 (1B)
unsigned long addr[9]; //list of ip address (4B/d)
}IP_OPT_HEADER,*PIP_OPT_HEADER; //length of total (39B)
typedef struct tagICMP_HEADER{
unsigned char type; //icmp type (1B)
unsigned char code; //code of type (1B)
unsigned short checksum; //checksum (2B)
unsigned short id; //identification (2B)
unsigned short seq; //sequence (2B)
unsigned long timestamp; // (2B)
//this is not standard header,but we reserve space for time
}ICMP_HEADER,*PICMP_HEADER; //total length (10B)
#define DEF_PACKET_SIZE 32
#define MAX_PACKET_SIZE 1024
#define ICMP_ECHO 8
#define ICMP_ECHOREPLY 0
#define IP_RECORD_ROUTER 7
void usageinfo(char *progname);
void FillIcmpData(char *icmp_data,int size);
USHORT CheckSum(USHORT *buf,int size);
void DecodeIcmpHeader(char *buf,int ret,LPSOCKADDR_IN lpSin);
void DecodeIpHeader(char *buf,int bytes);
//PING.CPP
#include "Ping.h"
int main(int argc,char *argv[])
{ //error:return -1,no err:return 0
if(argc==1)
{
usageinfo(argv[0]);
return -1;
}
BOOL bRecordRout =FALSE;
SOCKET hSocket =INVALID_SOCKET;
SOCKADDR_IN dstSin;
SOCKADDR_IN fromSin;
IP_OPT_HEADER ipOptHeader;
char* pIcmpData =NULL;
char* pRecvData =NULL;
char* lpDstIp =NULL;
int datasize =DEF_PACKET_SIZE;
int ret;
int rcvNum;
for(int i=1;i<argc;i++)
{
if(strchr(argv[i],'-')){
switch(tolower(argv[i][1])){
case 'r':
bRecordRout=TRUE;
break;
case 'd':
datasize=atoi(argv[i+1]);
i=argc+1; // to quit the for loop
break;
}
}
else if(strchr(argv[i],'.')){
int l=strlen(argv[i]);
if(l<7||l>15)
usageinfo(argv[0]);
else
lpDstIp=argv[i];
}
}
//initialize winsock
//
WSADATA wsaData;
WORD wVer=MAKEWORD(2,2);
if(WSAStartup(wVer,&wsaData)!=0){
printf("WSAStartup Error!\n");
return -1; //err,return -1,
}
//create socket handle
//
hSocket=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0,WSA_FLAG_OVERLAPPED);
if(hSocket==INVALID_SOCKET)
{
printf("WSASocket Error,Code:%d",WSAGetLastError());
WSACleanup();
return -1;
}
//set ip option header(record router)
if(bRecordRout)
{
ZeroMemory(&ipOptHeader,sizeof(ipOptHeader));
ipOptHeader.code=IP_RECORD_ROUTER;
ipOptHeader.len=39;
ipOptHeader.ptr=4;
if((ret=setsockopt(hSocket,IPPROTO_IP,IP_OPTIONS,(char *)&ipOptHeader,
sizeof(ipOptHeader)))==SOCKET_ERROR){
printf("setsockopt(IP_OPTIONS) error,code:%d",WSAGetLastError());
WSACleanup();
closesocket(hSocket);
return -1;
}
}
//set socket recv and send timeout;
int timeout=1000;
if((ret=setsockopt(hSocket,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(timeout)))==SOCKET_ERROR)
{
printf("setsockopt(SO_RCVTIMEO) error,code:%d",WSAGetLastError());
WSACleanup();
closesocket(hSocket);
return -1;
}
if((ret=setsockopt(hSocket,SOL_SOCKET,SO_SNDTIMEO,(char *)&timeout,sizeof(timeout)))==SOCKET_ERROR){
printf("setsockopt(SO_SNDTIMEO) error,code:%d",WSAGetLastError());
WSACleanup();
return -1;
}
pIcmpData=(char *)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,MAX_PACKET_SIZE);
pRecvData=(char *)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,MAX_PACKET_SIZE);
if(pIcmpData==NULL||pRecvData==NULL)
{
printf("HeapAlloc Error\n");
WSACleanup();
return -1;
}
datasize+=sizeof(ICMP_HEADER);
ZeroMemory(&dstSin,sizeof(dstSin));
dstSin.sin_family=AF_INET;
dstSin.sin_addr.s_addr=inet_addr(lpDstIp);
FillIcmpData(pIcmpData,datasize);
printf("Ping %s with %d bytes of data\n",inet_ntoa(dstSin.sin_addr),datasize);
int count=0;
int seq=0;
int rcvNum=0;
while(1)
{
count++;
if(count==5)
break;
((PICMP_HEADER)pIcmpData)->checksum=0;
((PICMP_HEADER)pIcmpData)->seq=seq++;
((PICMP_HEADER)pIcmpData)->timestamp=GetTickCount();
((PICMP_HEADER)pIcmpData)->checksum=
CheckSum((USHORT*)pIcmpData,datasize);
if((ret=sendto(hSocket,pIcmpData,datasize,0,(LPSOCKADDR)&dstSin,sizeof(dstSin)))==SOCKET_ERROR){
if(WSAGetLastError()==WSAETIMEDOUT){
printf("time out.\n");
continue;
}
else{
printf("sendto error,code:%d",WSAGetLastError());
closesocket(hSocket);
WSACleanup();
return -1;
}
}
int fromLen=sizeof(fromSin);
if((ret=recvfrom(hSocket,pRecvData,MAX_PACKET_SIZE,0,(sockaddr *)&fromSin,
&fromLen))==SOCKET_ERROR){
if(WSAGetLastError()==WSAETIMEDOUT)
{
printf("time out.\n");
continue;
}
printf("recvform fail!\n");
closesocket(hSocket);
WSACleanup();
return -1;
}
rcvNum++;
DecodeIcmpHeader(pRecvData,ret,&fromSin);
}
printf("\n Ping Statistics for : %s\n",lpDstIp);
printf("\t Send= %d, Received= %d,Lost= %d (%d%% loss)",4,rcvNum,4-rcvNum,(4-rcvNum)/4*100);
if(hSocket!=INVALID_SOCKET)
closesocket(hSocket);
HeapFree(GetProcessHeap(),0,pIcmpData);
HeapFree(GetProcessHeap(),0,pRecvData);
WSACleanup();
return 0;
}
void usageinfo(char *progname)
{
printf("Ping tool,by blode(blode@peoplemail.com.cn\n");
printf("usage:ping [-r] <host ip> [-d ][data size]\n");
printf("\t-r:\trecord router\n");
printf("\thost ip:\thost ip to ping\n");
printf("\t-d:\tuse data size option\n");
printf("\tdata size:\tdata size to ping(<=1024)\n");
}
void FillIcmpData(char *icmp_data,int size)
{
ICMP_HEADER *icmpHdr;
icmpHdr=(PICMP_HEADER)icmp_data;
icmpHdr->checksum=0;
icmpHdr->code=0;
icmpHdr->id=(unsigned short)GetCurrentProcessId();
icmpHdr->seq=0;
icmpHdr->type=ICMP_ECHO;
icmpHdr->timestamp=0;
}
USHORT CheckSum(USHORT *buf,int size)
{ //check sum function
USHORT cksum=0;
while(size>1)
{
cksum+=*buf++;
size-=sizeof(USHORT);
}
if(size)
cksum+=*buf++;
cksum=(cksum>>16)+(cksum&0xffff);
cksum+=(cksum>>16);
return (USHORT)(~cksum);
}
void DecodeIcmpHeader(char *buf,int ret,LPSOCKADDR_IN lpSin)
{
ICMP_HEADER *icmpHdr;
IP_HEADER *ipHdr;
int ipHdrLen;
static int first=0;
DWORD tick=GetTickCount();
ipHdr=(IP_HEADER*)buf;
ipHdrLen=ipHdr->h_len*4;
if(ipHdrLen==60&&!first)
DecodeIpHeader(buf,ret);
icmpHdr=(ICMP_HEADER *)(buf+ipHdrLen);
if(icmpHdr->type!=ICMP_ECHOREPLY){
printf("no echo reply %d recved\n",icmpHdr->type);
return;
}
if(icmpHdr->id!=(USHORT)GetCurrentProcessId()){
printf("someone else's packet!\n");
return;
}
printf("Reply from: %s",inet_ntoa(lpSin->sin_addr));
printf("\tbytes: %d icmp seq: %d TTL=128",ret,icmpHdr->seq);
printf(" time: %dms\n",tick-icmpHdr->timestamp);
first++;
return;
}
void DecodeIpHeader(char *buf,int bytes)
{
IP_OPT_HEADER *ipOptHdr;
IN_ADDR in;
ipOptHdr=(IP_OPT_HEADER*)(buf+20);
printf("Record Router: ");
for(int i=0;i<(ipOptHdr->ptr/4)-1;i++){
in.S_un.S_addr=ipOptHdr->addr[i];
printf("\t%-15s\n",inet_ntoa(in));
}
}
附录3 用Visual C++实现局域网IP多播
在局域网中,管理员常常需要将某条信息发送给一组用户。如果使用一对一的发送方法,虽然是可行的,但是过于麻烦,也常会出现漏发、错发。为了更有效的解决这种组通信问题,出现了一种多播技术(也常称为组播通信),它是基于IP层的通信技术。为了帮助读者理解,下面将简要的介绍一下多播的概念。
众所周知,普通IP通信是在一个发送者和一个接收者之间进行的,我们常把它称为点对点的通信,但对于有些应用,这种点对点的通信模式不能有效地满足实际应用的需求。例如:一个数字电话会议系统由多个会场组成,当在其中一个会场的参会人发言时,要求其它会场都能即时的得到此发言的内容,这是一个典型的一对多的通信应用,通常把这种一对多的通信称为多播通信。采用多播通信技术,不仅可以实现一个发送者和多个接收者之间进行通信的功能,而且可以有效减轻网络通信的负担,避免资源的无谓浪费。
广播也是一种实现一对多数据通信的模式,但广播与多播在实现方式上有所不同。广播是将数据从一个工作站发出,局域网内的其他所有工作站都能收到它。这一特征适用于无连接协议,因为LAN上的所有机器都可获得并处理广播消息。使用广播消息的不利之处是每台机器都必须对该消息进行处理。多播通信则不同,数据从一个工作站发出后,如果在其它LAN上的机器上面运行的进程表示对这些数据"有兴趣",多播数据才会发给它们。
本实例由Sender和Receiver两个程序组成,Sender用户从控制台上输入多播发送数据,Receiver端都要求加入同一个多播组,完成接收Sender发送的多播数据。
一、实现方法
1、 协议支持
并不是所有的协议都支持多播通信,对Win32平台而言,仅两种可从WinSock内访问的协议(IP/ATM)才提供了对多播通信的支持。因通常通信应用都建立在TCP/IP协议之上的,所以本文只针对IP协议来探讨多播通信技术。
支持多播通信的平台包括Windows CE 2.1、Windows 95、Windows 98、Windows NT 4、Windows 2000和WindowsXP。自2.1版开始,Windows CE才开始实现对IP多播的支持。本文实例建立在WindowsXP专业版平台上。
2、多播地址
IP采用D类地址来支持多播。每个D类地址代表一组主机。共有28位可用来标识小组。所以可以同时有多达25亿个小组。当一个进程向一个D类地址发送分组时,会尽最大的努力将它送给小组的所有成员,但不能保证全部送到。有些成员可能收不到这个分组。举个例子来说,假定五个节点都想通过I P多播,实现彼此间的通信,它们便可加入同一个组地址。全部加入之后,由一个节点发出的任何数据均会一模一样地复制一份,发给组内的每个成员,甚至包括始发数据的那个节点。D类I P地址范围在224.0.0.0到239.255.255.255之间。它分为两类:永久地址和临时地址。永久地址是为特殊用途而保留的。比如,224.0.0.0根本没有使用(也不能使用),224.0.0.1代表子网内的所有系统(主机),而224.0.0.2代表子网内的所有路由器。在 RFC 1700文件中,提供了所有保留地址的一个详细清单。该文件是为特殊用途保留的所有资源的一个列表,大家可以找来作为参考。"Internet分配数字专家组"(I A N A)负责着这个列表的维护。在表1中,我们总结了目前标定为"保留"的一些地址。临时组地址在使用前必须先创建,一个进程可以要求其主机加入特定的组,它也能要求其主机脱离该组。当主机上的最后一个进程脱离某个组后,该组地址就不再在这台主机中出现。每个主机都要记录它的进程当前属于哪个组。 表1 部分永久地址说明
地 址 说 明
224.0.0.1 基本地址(保留)
224.0.0.1 子网上的所有系统
224.0.0.2 子网上的所有路由器
224.0.0.5 子网上所有OSPF路由器
224.0.0.6 子网上所有指定的OSPF路由器
224.0.0.9 RIP第2版本组地址
224.0.1.1 网络时间协议
224.0.1.24 WINS服务器组地址
3、 多播路由器
多播由特殊的多播路由器来实现,多播路由器同时也可以是普通路由器。各个多播路由器每分钟发送一个硬件多播信息给子网上的主机(目的地址为224.0.0.1),要求它们报告其进程当前所属的是哪一组,各主机将它感兴趣的D类地址返回。这些询问和响应分组使用IGMP(Internet group management protocol),它大致类似于ICMP。它只有两种分组:询问和响应,都有一个简单的固定格式,其中有效载荷字段的第一个字段是一些控制信息,第二字段是一个D类地址,在RFC1112中有详细说明。
多播路由器的选择是通过生成树实现的,每个多播路由器采用修改过的距离矢量协议和其邻居交换信息,以便向每个路由器为每一组构造一个覆盖所有组员的生成树。在修剪生成树及删除无关路由器和网络时,用到了很多优化方法。
4.库支持
WinSock提供了实现多播通信的API函数调用。针对IP多播,WinSock提供了两种不同的实现方法,具体取决于使用的是哪个版本的 WinSock。第一种方法是WinSock1提供的,要求通过套接字选项来加入一个组;另一种方法是WinSock2提供的,它是引入一个新函数,专门负责多播组的加入,这个函数便是WSAJoinLeaf,它是基层协议无关的。本文将通过一个多播通信的实例的实现过程,来讲叙多播实现的主要步骤。因为Window98以后版本都安装了Winsock2.0以上版本,所以本文实例在WinSock2.0平台上开发的,但在其中对WinSock1实现不同的地方加以说明。
二、编程步骤
1、启动Visual C++6.0,创建一个控制台项目工程MultiCase。在此项目工程中添加Sender和Receiver两个项目。
Receiver项目实现步骤:
(1)、创建一个SOCK_DGRAM类型的Socket。
(2)、将此Socket绑定到本地的一个端口上,为了接收服务器端发送的多播数据。
(3)、加入多播组。
①、 WinSock2中引入一个WSAJoinLeaf,此函数原型如下:
SOCKET WSAJoinLeaf( SOCKET s, const struct sockaddr FAR *name, int namelen,
LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, DWORD dwFlags );
其中,第一个参数s代表一个套接字句柄,是自WSASocket返回的。传递进来的这个套接字必须使用恰当的多播标志进行创建;否则的话WSAJoinLeaf就会失败,并返回错误WSAEINVAL。第二个参数是SOCKADDR(套接字地址)结构,具体内容由当前采用的协议决定,对于IP协议来说,这个地址指定的是主机打算加入的那个多播组。第三个参数namelen(名字长度)是用于指定name参数的长度,以字节为单位。第四个参数lpCallerData(呼叫者数据)的作用是在会话建立之后,将一个数据缓冲区传输给自己通信的对方。第五个参数lpCalleeData(被叫者数据)用于初始化一个缓冲区,在会话建好之后,接收来自对方的数据。注意在当前的Windows平台上,lpCallerData和lpCalleeData这两个参数并未真正实现,所以均应设为NULL。LpSQOS和lpGQOS这两个参数是有关 Qos(服务质量)的设置,通常也设为NULL,有关Qos内容请参阅MSDN或有关书籍。最后一个参数dwFlags指出该主机是发送数据、接收数据或收发兼并。该参数可选值分别是:JL_SENDER_ONLY、JL_RECEIVER_ONLY或者JL_BOTH。
②、在WinSock1平台上加入多播组需要调用setsockopt函数,同时设置IP_ADD_MEMBERSHIP选项,指定想加入的那个组的地址结构。具体实现代码将在下面代码注释列出。
(4)、接收多播数据。
Sender实现步骤:
(1)、创建一个SOCK_DGRAM类型的Socket。
(2)、加入多播组。
(3)、发送多播数据。
3、编译两个项目,在局域网中按如下步骤测试:
(1)、将Sender.exe拷贝到发送多播数据的PC上。
(2)、将Receiver.exe拷贝到多个要求接收多播数据的PC上。
(3)、各自运行相应的程序。
(4)、在Sender PC上输入多播数据后,你就可以在Receiver PC上看到输入的多播数据。
//sender.cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#define MCASTADDR "233.0.0.1" //本例使用的多播组地址。
#define MCASTPORT 5150 //本地端口号。
#define BUFSIZE 1024 //发送数据缓冲大小。
int main( int argc,char ** argv)
{
WSADATA wsd;
struct sockaddr_in remote;
SOCKET sock,sockM;
TCHAR sendbuf[BUFSIZE];
int len = sizeof( struct sockaddr_in);
//初始化WinSock2.2
if( WSAStartup( MAKEWORD(2,2),&wsd) != 0 )
{
printf("WSAStartup() failed\n");
return -1;
}
if((sock=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,
WSA_FLAG_MULTIPOINT_C_LEAF|WSA_FLAG_MULTIPOINT_D_LEAF|
WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
{
printf("socket failed with:%d\n",WSAGetLastError());
WSACleanup();
return -1;
}
//加入多播组
remote.sin_family = AF_INET;
remote.sin_port = htons(MCASTPORT);
remote.sin_addr.s_addr = inet_addr( MCASTADDR );
if(( sockM = WSAJoinLeaf(sock,(SOCKADDR*)&remote,
sizeof(remote),NULL,NULL,NULL,NULL,
JL_BOTH)) == INVALID_SOCKET)
{
printf("WSAJoinLeaf() failed:%d\n",WSAGetLastError());
closesocket(sock);
WSACleanup();
return -1;
}
//发送多播数据,当用户在控制台输入"QUIT"时退出。
while(1)
{
printf("SEND : ");
scanf("%s",sendbuf);
if( sendto(sockM,(char*)sendbuf,strlen(sendbuf),0,(struct sockaddr*)
&remote,sizeof(remote))==SOCKET_ERROR)
{
printf("sendto failed with: %d\n",WSAGetLastError());
closesocket(sockM);
closesocket(sock);
WSACleanup();
return -1;
}
if(strcmp(sendbuf,"QUIT")==0) break;
Sleep(500);
}
closesocket(sockM);
closesocket(sock);
WSACleanup();
return 0;
}
//receiver.cpp
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#define MCASTADDR "233.0.0.1" //本例使用的多播组地址。
#define MCASTPORT 5150 //绑定的本地端口号。
#define BUFSIZE 1024 //接收数据缓冲大小。
int main( int argc,char ** argv)
{
WSADATA wsd;
struct sockaddr_in local,remote,from;
SOCKET sock,sockM;
TCHAR recvbuf[BUFSIZE];
/*struct ip_mreq mcast; // Winsock1.0 */
int len = sizeof( struct sockaddr_in);
int ret;
//初始化WinSock2.2
if( WSAStartup( MAKEWORD(2,2),&wsd) != 0 )
{
printf("WSAStartup() failed\n");
return -1;
}
/*
创建一个SOCK_DGRAM类型的SOCKET
其中,WSA_FLAG_MULTIPOINT_C_LEAF表示IP多播在控制面层上属于
"无根"类型;
WSA_FLAG_MULTIPOINT_D_LEAF表示IP多播在数据面层上属于"无根",
有关控制面层和
数据面层有关概念请参阅MSDN说明。
*/
if((sock=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,
WSA_FLAG_MULTIPOINT_C_LEAF|WSA_FLAG_MULTIPOINT_D_LEAF|
WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
{
printf("socket failed with:%d\n",WSAGetLastError());
WSACleanup();
return -1;
}
//将sock绑定到本机某端口上。
local.sin_family = AF_INET;
local.sin_port = htons(MCASTPORT);
local.sin_addr.s_addr = INADDR_ANY;
if( bind(sock,(struct sockaddr*)&local,sizeof(local)) == SOCKET_ERROR )
{
printf( "bind failed with:%d \n",WSAGetLastError());
closesocket(sock);
WSACleanup();
return -1;
}
//加入多播组
remote.sin_family = AF_INET;
remote.sin_port = htons(MCASTPORT);
remote.sin_addr.s_addr = inet_addr( MCASTADDR );
/* Winsock1.0 */
/*
mcast.imr_multiaddr.s_addr = inet_addr(MCASTADDR);
mcast.imr_interface.s_addr = INADDR_ANY;
if( setsockopt(sockM,IPPROTO_IP,IP_ADD_MEMBERSHIP,(char*)&mcast,
sizeof(mcast)) == SOCKET_ERROR)
{
printf("setsockopt(IP_ADD_MEMBERSHIP) failed:%d\n",WSAGetLastError());
closesocket(sockM);
WSACleanup();
return -1;
}
*/
/* Winsock2.0*/
if(( sockM = WSAJoinLeaf(sock,(SOCKADDR*)&remote,sizeof(remote),
NULL,NULL,NULL,NULL,
JL_BOTH)) == INVALID_SOCKET)
{
printf("WSAJoinLeaf() failed:%d\n",WSAGetLastError());
closesocket(sock);
WSACleanup();
return -1;
}
//接收多播数据,当接收到的数据为"QUIT"时退出。
while(1)
{
if(( ret = recvfrom(sock,recvbuf,BUFSIZE,0,
(struct sockaddr*)&from,&len)) == SOCKET_ERROR)
{
printf("recvfrom failed with:%d\n",WSAGetLastError());
closesocket(sockM);
closesocket(sock);
WSACleanup();
return -1;
}
if( strcmp(recvbuf,"QUIT") == 0 ) break;
else {
recvbuf[ret] = '\0';
printf("RECV:' %s ' FROM <%s> \n",recvbuf,inet_ntoa(from.sin_addr));
}
}
closesocket(sockM);
closesocket(sock);
WSACleanup();
return 0;
}
附录4:raw socket编程例子
1原始套接字工作原理与规则
原始套接字是一种不同于SOCK_STREAM和SOCK_DGRAM的套接字,它实现于系统核心。它的创建方式跟TCP/UDP创建方法几乎是一模一样,例如,通过
int sockfd;
sockfd=socktet(AF_INET,SOCK_RAW,IPPROTO_ICMP);
这两句程序你就可以创建一个原始套接字。这种类型套接字的功能与TCP或者UDP类型套接字的功能有很大的不同:TCP/UDP类型的套接字只能够访问传输层以及传输层以上的数据,因为当IP层把数据传递给传输层时,下层的数据包头已经被丢掉了。而原始套接字却可以访问传输层以下的数据,所以使用raw套接字你可以实现上至应用层的数据操作,也可以实现下至链路层的数据操作。比如:通过
sock=socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP))
方式创建的rawsocket就能直接读取链路层的数据。
1)使用原始套接字时应该注意的问题(参考<<unix网络编程>>以及网上的优秀文档)
(1):对于UDP/TCP产生的IP数据包,内核不将它传递给任何原始套接字,而只是将这些数据交给对应的UDP/TCP数据处理句柄(所以,如果你想要通过原始套接字来访问TCP/UDP或者其它类型的数据,调用socket函数创建原始套接字第三个参数应该指定为htons(ETH_P_IP),也就是通过直接访问数据链路层来实现。(我们后面的密码窃取器就是基于这种类型的)。
(2):对于ICMP和EGP等使用IP数据包承载数据但又在传输层之下的协议类型的IP数据包,内核不管是否已经有注册了的句柄来处理这些数据,都会将这些IP数据包复制一份传递给协议类型匹配的原始套接字。
(3):对于不能识别协议类型的数据包,内核进行必要的校验,然后会查看是否有类型匹配的原始套接字负责处理这些数据,如果有的话,就会将这些IP数据包复制一份传递给匹配的原始套接字,否则,内核将会丢弃这个IP数据包,并返回一个ICMP主机不可达的消息给源主机。
(4):如果原始套接字bind绑定了一个地址,核心只将目的地址为本机IP地址的数包传递给原始套接字,如果某个原始套接字没有bind地址,核心就会把收到的所有IP数据包发给这个原始套接字。
(5):如果原始套接字调用了connect函数,则核心只将源地址为connect连接的IP地址的IP数据包传递给这个原始套接字。
(6):如果原始套接字没有调用bind和connect函数,则核心会将所有协议匹配的IP数据包传递给这个原始套接字。
2)编程选项
原始套接字是直接使用IP协议的非面向连接的套接字,在这个套接字上可以调用bind和connect函数进行地址绑定。说明如下:
(1)bind函数:调用bind函数后,发送数据包的源IP地址将是bind函数指定的地址。如是不调用bind,则内核将以发送接口的主IP地址填充IP头。如果使用setsockopt设置了IP_HDRINCL(headerincluding)选项,就必须手工填充每个要发送的数据包的源IP地址,否则,内核将自动创建IP首部。
(2)connetc函数:调用connect函数后,就可以使用write和send函数来发送数据包,而且内核将会用这个绑定的地址填充IP数据包的目的IP地址,否则的话,则应使用sendto或sendmsg函数来发送数据包,并且要在函数参数中指定对方的IP地址。
综合以上种种功能和特点,我们可以使用原始套接字来实现很多功能,比如最基本的数据包分析,主机嗅探等。其实也可以使用原始套接字作一个自定义的传输层协议。
2一个简单的应用
下面的代码创建一个直接读取链路层数据包的原始套接字,并从中分析出源MAC地址和目的MAC地址,源IP和目的IP,以及对应的传输层协议,如果是TCP/UDP协议的话,打印其目的和源端口。为了方便阅读,程序中避免了使用任何与协议有关的数据结构,如structether_header,structiphdr等,当然,要完全理解代码,你需要关于指针以及位运算的知识。
/***************SimpelSniffer.c*************/
/*注意:本代码为LINUX操作系统下的源代码*/
//author:duanjigang@2006s
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#define BUFFER_MAX 2048
int main(int argc, char *argv[])
{
int sock, n_read, proto;
char buffer[BUFFER_MAX];
char *ethhead, *iphead, *tcphead,
*udphead, *icmphead, *p;
if((sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP))) < 0)
{
fprintf(stdout, "create socket error\n");
exit(0);
}
while(1)
{
n_read = recvfrom(sock, buffer, 2048, 0, NULL, NULL);
/*
14 6(dest)+6(source)+2(type or length)
+
20 ip header
+
8 icmp,tcp or udp header
= 42
*/
if(n_read < 42)
{
fprintf(stdout, "Incomplete header, packet corrupt\n");
continue;
}
ethhead = buffer;
p = ethhead;
int n = 0XFF;
printf("MAC: %.2X:%02X:%02X:%02X:%02X:%02X==>"
"%.2X:%.2X:%.2X:%.2X:%.2X:%.2X\n",
p[6]&n, p[7]&n, p[8]&n, p[9]&n, p[10]&n, p[11]&n,
p[0]&n, p[1]&n, p[2]&n,p[3]&n, p[4]&n, p[5]&n);
iphead = ethhead + 14;
p = iphead + 12;
printf("IP: %d.%d.%d.%d => %d.%d.%d.%d\n",
p[0]&0XFF, p[1]&0XFF, p[2]&0XFF, p[3]&0XFF,
p[4]&0XFF, p[5]&0XFF, p[6]&0XFF, p[7]&0XFF);
proto = (iphead + 9)[0];
p = iphead + 20;
printf("Protocol: ");
switch(proto)
{
case IPPROTO_ICMP: printf("ICMP\n");break;
case IPPROTO_IGMP: printf("IGMP\n");break;
case IPPROTO_IPIP: printf("IPIP\n");break;
case IPPROTO_TCP :
case IPPROTO_UDP :
printf("%s,", proto == IPPROTO_TCP ? "TCP": "UDP");
printf("source port: %u,",(p[0]<<8)&0XFF00 | p[1]&0XFF);
printf("dest port: %u\n", (p[2]<<8)&0XFF00 | p[3]&0XFF);
break;
case IPPROTO_RAW : printf("RAW\n");break;
default:printf("Unkown, please query in include/linux/in.h\n");
}
}
}
附录5:课程设计报告封面
计算机网络
课程设计报告
题目:
姓名:
学号:
班级:
指导老师:
湖南科技大学计算机科学与工程学院
20##年 月