编写UDP客户端和服务端集成到MenuOS里并跟踪分析UDP协议中数据包的收发处理过程
本实验来自科大孟宁老师教的《Linux网络程序设计》课程实验部分。
实验代码及参考:
https://github.com/mengning/linuxnet/blob/master/np2018.md
实验内容:
- 我们写一个UDP客户端和服务端单独执行,互相发送数据;
- 把UDP客户端和服务端集成到MenuOS里
- UDP sendto发送数据的过程
- 接收数据并解析放入队列
- UDP recvfrom接收数据的过程,应该是从接收队列里取出数据
实验目标:1)demo & 跟踪几个关键点;2)撰写一篇博客分析UDP协议的相关代码
实验平台:实验楼集成环境:https://www.shiyanlou.com/courses/1198
一、UDP客户端和服务端
UDP协议:
UDP套接口是无连接的、不可靠的数据报协议:
为什么使用呢?
其一:当应用程序使用广播或多播时只能使用UDP协议;其二:由于他是无连接的,所以速度快。
建立UDP套接口时socket函数的第二个参数应该是SOCK_DGRAM,说明是建立一个UDP套接口;由于UDP是无连接的,所以服务器端并不需要listen或accept函数。
基于UDP套接字编程的面向无连接的通信,它分为服务器端和客户端两部分,其主要实现过程如图所示。
分析:
1、socket函数:为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。
#include <sys/socket.h>
int socket(int family,int type,int protocol);
返回:非负描述字---成功 -1---失败
------------------------------
第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);
第二个参数指明套接口类型,有三种类型可选:
SOCK_STREAM(字节流套接口)、
SOCK_DGRAM(数据报套接口)和
SOCK_RAW(原始套接口);
如果套接口类型不是原始套接口,那么第三个参数就为0。
2、bind函数:为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
返回:0---成功 -1---失败
------------------------------------------------
第一个参数是socket函数返回的套接口描述字;第二和第第三个参数分别是一个
指向特定于协议的地址结构的指针和该地址结构的长度。
3、recvfrom函数:UDP使用recvfrom()函数接收数据,他类似于标准的read(),但是在recvfrom()函数中要指明目的地址。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * from, size_t *addrlen);
返回接收到数据的长度---成功 -1---失败
-------------------------------------------------------------------
前三个参数等同于函数read()的前三个参数,
flags参数是传输控制标志。
最后两个参数类似于accept的最后两个参数。
4、sendto函数:UDP使用sendto()函数发送数据,他类似于标准的write(),但是在sendto()函数中要指明目的地址。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr * to, int addrlen);
返回发送数据的长度---成功 -1---失败
-------------------------------------------------------------------
前三个参数等同于函数read()的前三个参数,
flags参数是传输控制标志。参数to指明数据将发往的协议地址,
他的大小由addrlen参数来指定。
二、把UDP客户端和服务端集成到MenuOS里
- 下图为udp的收发过程:
参考示例(udpserver.c)
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <netdb.h>
#define PORT 1234
#define MAXDATASIZE 100
int start_udp_server()
{
int sockfd;
struct sockaddr_in server;
struct sockaddr_in client;
socklen_t addrlen;
int num;
char buf[MAXDATASIZE];
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("Creatingsocket failed.");
exit(1);
}
bzero(&server,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(PORT);
server.sin_addr.s_addr= htonl (INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&server, sizeof(server)) == -1)
{
perror("Bind()error.");
exit(1);
}
addrlen=sizeof(client);
while(1)
{
num =recvfrom(sockfd,buf,MAXDATASIZE,0,(struct sockaddr*)&client,&addrlen);
if (num < 0)
{
perror("recvfrom() error\n");
exit(1);
}
buf[num] = '\0';
printf("Recieve a message from client(ip:%s, port:%d.)\n message is %s.\n ",inet_ntoa(client.sin_addr),htons(client.sin_port),buf);
sendto(sockfd,"Welcome!.\n",22,0,(struct sockaddr *)&client,addrlen);
if(!strcmp(buf,"bye"))
break;
}
close(sockfd);
}
udp客户端实现的功能
1、客户根据用户提供的IP地址将用户从终端输入的信息发送给服务器,然后等待服务器的回应。
2、服务器接收客户端发送的信息并显示,同时显示客户的IP地址、端口号,并向客户端发送信息。如果服务器接收的客户信息为“bye”,则退出循环,并关闭套接字。
3、客户接收、显示服务器发回的信息,并关闭套接字。
参考程序(udpclient.c)
int start_udp_client(int argc, char *argv[])
{
int sockfd, num;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in server,peer;
if (argc !=3)
{
printf("Usage: %s <IP Address><message>\n",argv[0]);
exit(1);
}
if ((he=gethostbyname(argv[1]))==NULL)
{
printf("gethostbyname()error\n");
exit(1);
}
if ((sockfd=socket(AF_INET, SOCK_DGRAM,0))==-1)
{
printf("socket() error\n");
exit(1);
}
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr= *((struct in_addr *)he->h_addr);
sendto(sockfd, argv[2],strlen(argv[2]),0,(struct sockaddr *)&server,sizeof(server));
socklen_t addrlen;
addrlen=sizeof(server);
while (1)
{
if((num=recvfrom(sockfd,buf,MAXDATASIZE,0,(struct sockaddr *)&peer,&addrlen))== -1)
{
printf("recvfrom() error\n");
exit(1);
}
if (addrlen != sizeof(server) ||memcmp((const void *)&server, (const void *)&peer,addrlen) != 0)
{
printf("Receive message from otherserver.\n");
continue;
}
buf[num]='\0';
printf("Return a message from server:%s\n",buf);
break;
}
close(sockfd);
}
三、实验步骤
编写Linux下UDP服务器套接字程序,服务器接收客户端发送的信息并显示,同时显示客户的IP地址、端口号,并向客户端发送信息。如果服务器接收的客户信息为“bye”,则退出循环,并关闭套接字
- 在实验楼平台:下载老师提供的实验代码;
cd LinuxKernel
git clone https://github.com/mengning/linuxnet.git
cd linuxnet/lab3
- 将udpserver.c 和 udpclient.c 集成到 MenuOS中。
- 修改lab3下 main.c的代码。
- 在main函数中加入menuconfig 项。
MenuConfig("udpserver", "udp server", start_udp_server);
MenuConfig("udpclient", "udp client", start_udp_client);
- 执行
make rootfs
- 测试
- 新建terminal. 输入
./init udpserver udpclient 127.0.0.1 hello
- 结果如下:
- 在client端执行命令:
udpclient 127.0.0.1 bye
四、UDP sendto发送数据的过程
gdb 的调试过程
file vmlinux
target remote:1234
c
b sys_sendto
menuos 运行 udpserver, udpclient
进入断点
Breakpoint 1, inet_sendmsg (iocb=0xc7859cbc, sock=0xc763cc00, msg=0xc7859d54, size=1024) at net/ipv4/af_inet.c:723
http://codelab.shiyanlou.com/source/xref/linux-3.18.6/net/ipv4/af_inet.c#721
即进入 inet_sendmsg
b net/ipv4/af_inet.c#733
s (step into)
会进入 net/ipv4/udp.c:878
http://codelab.shiyanlou.com/source/xref/linux-3.18.6/net/ipv4/udp.c#863
bt 可以查看函数调用栈
关键词 881 行的
struct sk_buff *skb; // socket buff, 也就是要发送的包
在代码中搜索 skb,下一次出现是在 1039 行
skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, &rt, msg->msg_flags);
cork 是木塞的意思
net/ipv4/udp.c#879
int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
net/ipv4/udp.c#1037
/* Lockless fast path for the non-corking case. */
if( !corkreq ){
skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen, sizeof(struct udphdr), &ipc, &rt, msg->msg_flags);
}
b net/ipv4/udp.c:1044
c
si
si
进入 net/ipv4/udp.c:784
static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
print *skb
五、UDP recvfrom接收数据的过程
UDP报文的接收可以分为两个部分:
- 协议栈收到udp报文,插入相应队列中;
- 用户调用recvfrom()或recv()系统调用从队列中取出报文,
- 这里的队列就是sk->sk_receive_queue用户调用recvfrom()或recv()系统调用从队列中取出报文
5.1 UDP报文发送
发送时有两种调用方式:sys_send()和sys_sendto(),两者的区别在于sys_sendto()需要给入目的地址的参数;而sys_send()调用前需要调用sys_connect()来绑定目的地址信息;两者的后续调用是相同的。如果调用sys_sendto()发送,地址信息在sys_sendto()中从用户空间拷贝到内核空间,而报文内容在udp_sendmsg()中从用户空间拷贝到内核空间。
send_to() -> sys_sendto()->sys_sendmsg()->sock-ops->sendmsg()
==> inet_sendmsg()->sk->sk_prot_sendmsg()
==>udp_sendmsg()
5.1 UDP报文接收
-
协议栈如何收取udp报文的。
udp模块的注册在inet_init()中,当收到的是udp报文,会调用udp_protocol中的handler函数udp_rcv()。
udp_rcv() -> __udp4_lib_rcv() 完成udp报文接收 -
用户如何收取报文
用户可以调用sys_recvfrom()或sys_recv()来接收报文,所不同的是,sys_recvfrom()可能通过参数获得报文的来源地址,而sys_recv()则不可以,但对接收报文并没有影响。在用户调用recvfrom()或recv()接收报文前,发给该socket的报文都会被添加到sk->sk_receive_queue上,recvfrom()和recv()要做的就是从sk_receive_queue上取出报文,拷贝到用户空间,供用户使用。
sys_recv() -> sys_recvfrom()
sys_recvfrom() -> sk->ops->recvmsg()
==> sock_common_recvmsg() -> sk->sk_prot->recvmsg()
==> udp_recvmsg()
sys_recvfrom()
调用sock_recvmsg()接收udp报文,存放在msg中,如果接收到报文,从内核到用户空间拷贝报文的源地址到addr中,addr是recvfrom()调用的传入参数,表示报文源的地址。而报文的内容是在udp_recvmsg()中从内核拷贝到用户空间的。