计算机网络实验
题目:Socket编程
实验1 基于UDP的Socket编程
实验2 基于TCP的Socket编程
目录
一、实验内容:............................................................................................................... 3
实验1 基于UDP的Socket编程................................................................................ 3
实验2 基于TCP的Socket编程................................................................................. 3
二、 程序设计................................................................................................................. 3
1 基于UDP的Socket编程...................................................................................... 3
1.1 实验原理:基于UDP协议的无连接C/S的工作流程..................................... 3
1.2 实现方案和实验结果.................................................................................... 4
1.2.1 方案1:Linux上两个进程间的通信........................................................... 4
方案1实现方案.......................................................................................... 4
方案1实验结果.......................................................................................... 4
1.2.2 方案2:Windows主机(client)和Linux虚拟机(server)之间的进程通信 6
方案2实现方案.......................................................................................... 6
方案2实验结果.......................................................................................... 6
2 基于TCP的Socket编程....................................................................................... 7
2.1 实验原理:基于TCP协议的面向C/S的工作流程......................................... 7
2.2 实现方案..................................................................................................... 8
2.3 实验结果..................................................................................................... 9
三、讨论与心得............................................................................................................. 10
四、源代码.................................................................................................................... 12
一、实验内容:
本单元实验包括的实验项目主要是UDP Socket编程和TCP Socket编程。本单元实验的主要目的是希望通过本单元实验,让学员掌握Socket编程方法,同时通过本单元实验让学生掌握如何开发基于TCP/IP协议的网络应用。
实验1 基于UDP的Socket编程
l 实验目的:通过本实验使学员了解和掌握编写基于UDP协议的网络应用程序。
l 实验内容:编写基于UDP协议网络聊天程序,要求发送程序和接收程序能够接收键盘输入并彼此之间相互发送数据。
l 实验步骤:
1.编写server端程序;
2.编写client端程序;
3.client程序与server程序联调;
4.client程序与server程序相互通信。
实验2 基于TCP的Socket编程
l 实验目的:通过本实验使学员了解和掌握编写基于TCP协议的网络应用程序。
l 实验内容:编写基于TCP协议网络聊天程序,要求发送程序和接收程序能够接收键盘输入并彼此之间相互发送数据。
l 实验步骤同UDP实验。
二、程序设计
1 基于UDP的Socket编程
1.1 实验原理:基于UDP协议的无连接C/S的工作流程
在server端,server首先启动,调用socket( )创建套接字,然后调用bind( )绑定server的地址(IP+port),调用recvfrom( )等待接收数据。
在client端,先调用socket()创建套接字,调用sendto( )向server发送数据。
server接收到client发来数据后,调用sendto( )向client发送应答数据,client调用recv接收server发来的应答数据。数据传输结束,server和client通过调用close( )关闭套接字。
原理图如图1。
图1 UDP通信原理图
1.2 实现方案和实验结果
1.2.1 方案1:Linux上两个进程间的通信
方案1实现方案
Ø server端:
首先调用socket函数创建一个socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,用recvfrom函数接收到一个client端的消息后,用printf打印出来,server从标准输入设备中取得一行字符串后,调用sendto函数发送给client端。最后用close关闭该socket。
Ø client机端:
首先调用socket函数创建一个socket,初始化server地址及端口号(实际上就是本机),从标准输入设备中取得字符串,用sendto传送给server端,然后用recv函数接收server端发来的字符串,用printf打印出来。最后用close关闭该socket。
源代码见附录。
方案1实验结果
server和client通信截图
图2 UDPserver
图3 UDPClient
1.2.2 方案2:Windows主机(client)和Linux虚拟机(server)之间的进程通信
方案2实现方案
利用Vmware station的Linux虚拟机与Windows本机之间的局域网进行进程间通信,Linux虚拟机作为server,Windows本机作为client。除了Windows使用winsock函数库之外,实现方案与方案1类似,但要注意client端初始化的server地址虽然与方案1相同,但此时已不再是本机地址,而是局域网中Linux虚拟机的IP地址,相当于模拟了不同机器间的进程通信。
方案2实验结果
server和client通信截图
图4 server
图5 client
2 基于TCP的Socket编程
2.1 实验原理:基于TCP协议的面向C/S的工作流程
在server端,server首先启动,调用socket( )创建套接字;然后调用bind( ) 绑定server的地址(IP+port);再调用listen( )让server做好侦听准备,并规定好请求队列长度,然后server进入阻塞状态,等待client的连接请求;最后通过accept( )来接收连接请求,并获得client的地址。当accpet接收到一个client发来的connet请求时,将生成一个新的socket,用于传输数据。
在client端,client在创建套接字并指定client的socket地址,然后就调用connect( )和server建立连接。一旦连接建立成功,client和server之间就可以通过调用recv和send来接收和发送数据。一旦数据传输结束,server和client通过调用close( )来关闭套接字。
原理图如图6。
图6
2.2 实现方案
Ø server端:
首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,然后调用listen在相应的socket上监听client,当accpet接收到一个client发来的connet请求时,将生成一个新的socket,用于传输数据。Recv( )接收到一个client端的消息后,server从标准输入设备中取得一行字符串,调用send()发送给client端。
Ø client端:
首先调用socket函数创建一个Socket,初始化server地址及端口号,然后调用connet请求连接监听server。通过server端accept函数生成的新socket,调用send函数向server端发送从标准输入设备中取得的字符串,然后用recv接收server端发来的字符串。最后用close()关闭该数据传输socket和监听socket。
源代码见附录。
2.3 实验结果
server和client通信截图
图7 TCPserver
图8 TCPserver
三、讨论与心得
记得助教第一节课讲socket的时候,我听得云里雾里的,只觉得是个很神奇的可以实现进程间通信的类似于接口的东西。印象深刻的是他举的那个打电话的例子,描述得很直观形象。老师说让我们做这个实验主要是为了体会TCP/IP的原理,并不要求应用做得多么高端,比如图形界面什么的。我觉得,我既然是网络工程专业的,就更应该学好计算机网络这门课,更好地了解socket编程。抱着这种决心,我开始了学习。
于是我先去网上找了有关socket编程的讲解,觉得一篇《一切皆socket》的帖子写得很易懂,对于各个函数的功能有了初步的了解。操作系统的课程也有一个做shell命令解释器的实验,与socket编程实验基本是先后进行的。在做了shell,熟悉了“一切皆文件”的linux以及编译、运行的方法之后,感觉跟好理解,更好入手了。
我先对照着《一切皆socket》的帖子看老师给的资料附录一中的示例一、二,也就是TCP。原来socket()是用来创建一个,然后返回值是这个socket的描述符,很类似与文件描述符。bind()是用来为这个socket绑定server的IP+port地址,当然绑定之前要先对server的地址进行初始化。listen()的作用是监听client的请求,第二个参数规定了这个刚刚创建的socket可以接受几个client的服务请求。一旦accept()了一个client的请求,就获得了这个client的地址,这真是个很神奇的东东啊!操作系统就会又给它分配了一个socket,也就是accept的返回值,用来传输数据。也就是说,TCP开了2个socket,一个用来监听子进程,一个用来传输数据。accept之后就可以自由发挥,开始进行网络I/0了!我们运行程序时可以看到的功能就是在下面的部分实现的。再往下的收发函数,read()/write(),recv()/send(),readv()/writev(),recvfrom()/sendto(),recvmsg()/sendmsg()什么的,就是用数据传输的socket了,而非监听的socket了。关于收发函数,我又上网查了一下资料。拿recv()/send()举例,其实recv只是把TCP协议传过来的数据copy到我们自己定义的buf里去。而recv所做的把发送缓冲区里的数据被协议发送到接收缓冲区这个过程对于编程用户来讲是不可见的,而recv只是把接收缓冲区里的数据copy出来而已。Send与之类似。从这个层面上讲,socket是一个TCP协议提供的接口服务,用传输层的协议干应用层想干的事。最后不要忘记用close()关掉创建的两个socket。此外,我大概理解了TCP的三次握手在函数中的体现,要传三个报文,如图1。
图9 TCP的三次握手建立连接
看懂、理解了代码后,我把代码拷了进去,调试成功后,看到了server会把client发过来的数据再返回去,我觉得这个没什么意义,就想改编它,让server也可以通过输入回答client。于是就又定义了一个buf3,用gets函数接受标准输入到buf3里,然后用把buf3的内容发给client。
一个偶然的发现让我理解了TCP所谓的四次握手释放过程。当我把client关掉时,server就自动关掉了。当client首先调用close主动关闭连接,这时TCP发送一个FIN M;另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给server的应用程序,因为FIN的接收意味着server的应用进程在相应的连接上再也接收不到额外数据;一段时间之后,server上接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;接收到这个FIN的源发送端TCP对它进行确认。这样每个方向上都有一个FIN和ACK。
图10 socket中发送的TCP四次握手释放
调通了TCP之后,我去尝试UDP。其实这顺序是反了的,因为UDP貌似更简单一些,可是因为TCP有现成的代码,所以就先理解了TCP。之后去查UDP的资料,直观上感觉除了创建socket()时定义的协议不同外,就是少了一些函数,少用了一个socket。对于server来讲,bind()之后,不用再listen和accept了,就是不用监听了,直接用这个socket来进行数据传输就可以了。于是我就想当然地觉得下面进行网络I/O的收发函数应该是差不多的,就直接用了刚才在TCP里用的recv和send。对于client来讲,我也是直接用的这两个函数。然后怎么都传不了数据,实现不了通信。于是我仔细比较了几个函数的参数,发现recvfrom比recv多了2个参数,后来理解了一下,这是因为TCP在accept时就为与之通信的每个进程分配了一个socket用于数据传输,也就是说对于server的每个client都有一个专门的进程与其进行通信。而UDP没有开另外的进程,也就是没有创建新的socket,所以server的多个client是共用这一个socket的,可以说是多路复用了。为了辨别多个client,server需要client的地址(ip+port)来接受和发送,于是就要用recvfrom多出来的2个参数,即client的地址和地址长度。这也就同时解释了为什么UDP只创建一个socket就够了。对于UDP的client端来说,只需要在发送数据的时候用sendto来表明server的地址,在接收数据时就不需要的了,因为只有一个server,不需要标记了。
因为我是在虚拟机上进行的以上实验,我就想利用主机和虚拟机之间的局域网来建立连接,进行进程通信。虽然操作系统不同,socket库不同,但是只要使用了相同的协议(我选择的是UDP)就可以进行通信。于是我又去学习了Winsock的相关知识,实现了不同系统间的通信。我觉得虽然Windows和Linux的socket使用总体上差不多,但是Winsock的一些准备(我觉得比较冗余)复杂一点。
四、源代码
【UDP方案1】
Server端源码:
//udpService:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#define PORT 7000
int main(void)
{
int sockfd,pktlen;
char buf[300],buf1[300];
struct sockaddr_in server,client;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
memset ((char *)&server, sizeof(server), 0); //将已开辟内存空间 server 的全部字节的值设为值0.类似于bzero
server.sin_family = AF_INET;
server.sin_port = htons(PORT);//端口号
server.sin_addr.s_addr = INADDR_ANY;//设置网络地址,INADDR_ANY表示机器的IP地址
bind(sockfd,(struct sockaddr *)&server,sizeof(struct sockaddr_in));
for (;;) {
/*recv接受client发送的数据,recv函数仅仅是copy数据,真正的接收数据是协议来完成的),
第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度
recv函数返回其实际copy的字节数*/
int l=sizeof(struct sockaddr_in);
pktlen = recvfrom (sockfd, buf, sizeof (buf), 0,(struct sockaddr_in *)&client,&l);
if (pktlen == 0)
break;
printf ("Received line: %s\n", buf);
printf ("Enter a line: ");
fgets(buf1,300,stdin);
/*并不是send把ns的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到ns的发送缓冲区的剩余空间里
返回实际copy的字节数*/
sendto (sockfd, buf1,sizeof(buf1), 0,(struct sockaddr_in*)&client,l);
}
close(sockfd);
}
Client端源码:
//udpClient:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define MAX_SIZE 1024
#define PORT 7000
#define HOST_ADDR "192.168.145.137"
int main(int argc,char **argv)
{
int sockfd,buflen;
char buf1[300],buf2[300];
struct sockaddr_in server;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
server.sin_family=AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = inet_addr (HOST_ADDR);
for(;;)
{
printf ("Enter a line: ");
gets (buf1);//从stdin流中读取字符串,直至接受到换行符
buflen = strlen (buf1);
if (buflen == 0)
break;
sendto(sockfd, buf1, buflen + 1, 0,(struct sockaddr *)&server,sizeof(server));
recv(sockfd, buf2, sizeof (buf2), 0);
printf("Received line: %s\n", buf2);
}
close(sockfd);
return 0;
}
【UDP方案2】
Linux server端源码同方案1
Windows client端源码:
//客户端
#include <Winsock2.h>
#include <stdio.h>
#define INVALID_VALUE32 0xFFFF
#pragma comment(lib, "ws2_32.lib")
//客户端主函数开始
void main()
{
int err = INVALID_VALUE32;
WORD ver;
WSADATA wsaData;
//定义版本号
ver = MAKEWORD(2, 2); /* 版本号为2.2版本,1.1也可以 */
/* 接下来初始化 */
err = WSAStartup(ver, &wsaData);
if (err != 0) /* 检查socket是否初始化成功 */
{
return;
}
if (2 != (LOBYTE(wsaData.wVersion)) || (2 != HIBYTE(wsaData.wVersion)))
{
WSACleanup(); /* 版本错误则清楚导入的DLL */
return;
}
/* 开始socket的主体部分:创建socket */
SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0); /* AF_INET表示网络使用的范围internetwork: UDP, TCP, etc,
SOCK_STREAM表示使用的是TCP
SOCK_DGRAM 表示使用的是UDP类型 */
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.145.137"); // 这里设置为服务器的IP地址,由于我在自己的机器上面写的,所以写回环地址了
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(7000); /* 端口随便只要不跟系统冲突就行 */
/* 定义几个缓冲区,也是接受和发送的,设置成100即可 */
char recvbuf[100];
char sendbuf[100];
int len=sizeof(SOCKADDR);
/* 下面进入死循环 */
while (1)
{
printf("\nEnter a line:\n");
gets(sendbuf);
sendto(sockClient, sendbuf, strlen(sendbuf)+1, 0, (SOCKADDR *)&addrSrv, len);
int recvnum = recvfrom(sockClient, recvbuf, 100, 0, (SOCKADDR *)&addrSrv, &len);
recvbuf[recvnum] = 0;
/* 将接收到的数据存入数据缓冲区,以便显示 */
printf("Receive a line %s", recvbuf);
}
/* 最后将socket清空 */
closesocket(sockClient);
WSACleanup();
return;
}
【TCP】
Server端源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <errno.h>
#define PORT 7000
main ()
{
struct sockaddr_in client, server;// 客户端地址信息 本机地址信息
int s, ns, namelen, pktlen;//s:监听socket ns:数据传输socket namelen:client的地址长度 pktlen:传送数据的字节数
char buf[400];
char buf3[200];
s=socket(AF_INET, SOCK_STREAM, 0); //创建连接的SOCKET,s为socket描述符
// 初始化服务器地址
memset ((char *)&server, sizeof(server), 0); //将已开辟内存空间 server 的全部字节的值设为值0.类似于bzero
server.sin_family = AF_INET;
server.sin_port = htons(PORT);//端口号
server.sin_addr.s_addr = INADDR_ANY;//设置网络地址,INADDR_ANY表示机器的IP地址
//server需要在listen之前绑定一个大家都知道的地址,就是刚刚初始化好的ip+端口号
bind(s, (struct sockaddr *)&server, sizeof(server));
listen(s,1);//侦听客户端请求,i为socket可以排队链接的最大个数
/*接受client请求,s为server的描述符(即监听socket描述符),第二个参数即指针client的协议地址,第三个参数代表地址长度
返回值ns是一个全新的描述符,是数据传输socket,代表与返回客户的tcp连接*/
namelen = sizeof (client);
ns = accept (s, (struct sockaddr *)&client, &namelen);
//开始进行网络I/O
for (;;) {
/*recv接受client发送的数据,recv函数仅仅是copy数据,真正的接收数据是协议来完成的),
第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度
recv函数返回其实际copy的字节数*/
pktlen = recv (ns, buf, sizeof (buf), 0);
if (pktlen == 0)
break;
printf ("Received line: %s\n", buf);
printf ("Enter a line: ");
gets(buf3);
/*并不是send把ns的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到ns的发送缓冲区的剩余空间里
返回实际copy的字节数*/
send (ns, buf3,sizeof(buf3), 0);
}
close(ns);
close(s);
}
client端源码:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#define PORT 7000
#define HOST_ADDR "192.168.145.137"
int main ()
{
struct sockaddr_in server;
int s, ns;
int pktlen, buflen;
char buf1[256], buf2[256];
s=socket(AF_INET, SOCK_STREAM, 0);
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = inet_addr (HOST_ADDR);
//connect第一个参数是client的socket描述符,第二个参数是server的socket地址,第三个为地址长度
if (connect(s, (struct sockaddr *)&server, sizeof(server)) < 0)
{
perror("connect()");
return;
}
//进行网络I/O
for (;;) {
printf ("Enter a line: ");
gets (buf1);//从stdin流中读取字符串,直至接受到换行符
buflen = strlen (buf1);
if (buflen == 0)
break;
send(s, buf1, buflen + 1, 0);
recv(s, buf2, sizeof (buf2), 0);
printf("Received line: %s\n", buf2);
}
close(s);
return 0;
}