《庖丁解牛Linux网络核心》实验一:跟踪UDP协议的收发过程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jason_holan/article/details/85119483

编写UDP客户端和服务端集成到MenuOS里并跟踪分析UDP协议中数据包的收发处理过程

本实验来自科大孟宁老师教的《Linux网络程序设计》课程实验部分。
实验代码及参考:
https://github.com/mengning/linuxnet/blob/master/np2018.md

实验内容:

  1. 我们写一个UDP客户端和服务端单独执行,互相发送数据;
  2. 把UDP客户端和服务端集成到MenuOS里
  3. UDP sendto发送数据的过程
  4. 接收数据并解析放入队列
  5. 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”,则退出循环,并关闭套接字

  1. 在实验楼平台:下载老师提供的实验代码;
cd LinuxKernel 
git clone https://github.com/mengning/linuxnet.git
cd linuxnet/lab3
  1. 将udpserver.c 和 udpclient.c 集成到 MenuOS中。
  2. 修改lab3下 main.c的代码。
  3. 在main函数中加入menuconfig 项。
	MenuConfig("udpserver", "udp server", start_udp_server);
	MenuConfig("udpclient", "udp client", start_udp_client);
  1. 执行 make rootfs
  2. 测试
    • 新建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报文的接收可以分为两个部分:

  1. 协议栈收到udp报文,插入相应队列中;
  2. 用户调用recvfrom()或recv()系统调用从队列中取出报文,
  3. 这里的队列就是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报文接收
  1. 协议栈如何收取udp报文的。
    udp模块的注册在inet_init()中,当收到的是udp报文,会调用udp_protocol中的handler函数udp_rcv()。
    udp_rcv() -> __udp4_lib_rcv() 完成udp报文接收

  2. 用户如何收取报文
    用户可以调用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()中从内核拷贝到用户空间的。

猜你喜欢

转载自blog.csdn.net/jason_holan/article/details/85119483