深入理解TCP协议及其源代码
2019-12-24 21:06 AI469 阅读(597) 评论(0) 收藏 举报一、TCP协议的初始化
1、TCP简介
传输控制协议TCP是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP层是位于IP层之上,应用层之下的传输层。
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分割成适当长度的报文段。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个字节一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认;如果发送端实体在合理的往返时延内未收到确认,那么对应的数据将会被重传。TCP用一个校验和函数来检验数据是否有错误,并且在发送和接收时都要计算和校验。
2、TCP/IP简介
I因特网协议,IP协议位于网络层,IP协议规定了数据传输时的基本单元和格式,IP协议还定义了数据包的递交办法和路由选择。IP层接收由更低层发来的数据包,并把该数据包发送到更高层—TCP层;相反,IP层也把从TCP接收来的数据包传送到更低层。当我们讨论TCP协议的初始化时自然离不开IP协议。TCP/IP是一个工业标准的协议集,显而易见这意味着 TCP 和 IP 在一起协同工作。
3、初始化
TCP/IP协议的初始化函数为inet_inet,由fs_initcall(inet_init); 在系统启动时,自动调用。
static int __init inet_init(void) { struct sk_buff *dummy_skb; struct inet_protosw *q; struct list_head *r; int rc = -EINVAL; BUILD_BUG_ON(sizeof(struct inet_skb_parm) > sizeof(dummy_skb->cb)); /* 申请reserved ports的bitmap */ sysctl_local_reserved_ports = kzalloc(65536 / 8, GFP_KERNEL); if (!sysctl_local_reserved_ports) goto out; /* 注册TCP,UDP和RAW协议 */ rc = proto_register(&tcp_prot, 1); if (rc) goto out_free_reserved_ports; rc = proto_register(&udp_prot, 1); if (rc) goto out_unregister_tcp_proto; rc = proto_register(&raw_prot, 1); if (rc) goto out_unregister_udp_proto; /* * Tell SOCKET that we are alive... */ /* 注册Inet familiy*/ (void)sock_register(&inet_family_ops); #ifdef CONFIG_SYSCTL ip_static_sysctl_init(); #endif /* Add all the base protocols. */ /* 添加协议:ICMP,UDP,TCP和IGMP。 从这里就可以看出,内核中支持的TCP/IP协议种类 */ if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0) printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n"); if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0) printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n"); if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0) printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n"); #ifdef CONFIG_IP_MULTICAST if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0) printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n"); #endif /* Register the socket-side information for inet_create. */ /* 初始化inetsw */ for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r) INIT_LIST_HEAD(r); /* 将inetsw_array中的协议挂载到inetsw上 */ for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q) inet_register_protosw(q); /* 下面是各个Inet模块的初始化。 */ /* Set the ARP module up */ arp_init(); /* Set the IP module up */ ip_init(); tcp_v4_init(); /* Setup TCP slab cache for open requests. */ tcp_init(); /* Setup UDP memory threshold */ udp_init(); /* Add UDP-Lite (RFC 3828) */ udplite4_register(); /* Set the ICMP layer up */ if (icmp_init() < 0) panic("Failed to create the ICMP control socket.\n"); /* Initialise the multicast router */ #if defined(CONFIG_IP_MROUTE) if (ip_mr_init()) printk(KERN_CRIT "inet_init: Cannot init ipv4 mroute\n"); #endif /* Initialise per-cpu ipv4 mibs */ if (init_ipv4_mibs()) printk(KERN_CRIT "inet_init: Cannot init ipv4 mibs\n"); ipv4_proc_init(); ipfrag_init(); /* 这个将IP协议注册L2层 */ dev_add_pack(&ip_packet_type); rc = 0; out: return rc; out_unregister_udp_proto: proto_unregister(&udp_prot); out_unregister_tcp_proto: proto_unregister(&tcp_prot); out_free_reserved_ports: kfree(sysctl_local_reserved_ports); goto out; }
- proto_register:这个操作主要是为了将注册协议,挂载到/proc文件系统上。通过/proc/net/protocols可以看到注册协议的统计信息;
- sock_register;这个操作是注册socket family。也就是socket(2)中的第一个参数所指的family;这样通过family就可以调用对应family的回调函数;
- inet_register_protosw:这个操作从名字上看是注册proto,其实我个人觉得实际上是根据socket type注册。这个type也是对应于socket(2)中第二个参数socket_type。这个注册支持重复type,但是如果以前已经存在permanet的type且新的proto与原有的proto协议相同,就会报告冲突。
- dev_add_pack(&ip_packet_type):这个操作是将IP协议注册到2层(ptype_base)当中。当2层协议与ip_packet_type.type(cpu_to_be16(ETH_P_IP))相等时,即收到的2层包的payload为IP协议,即调用ip_packet_type对应的回调函数。
- 当inet_init执行完时,基本的TCP/IP协议已经挂载完毕。
二、socket创建TCP套接字描述符
1、套接字(socket)
套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。实质上socket就是一种为了完成两个应用程序之间的数据传输独立于协议的网络编程接口。
套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据传输要通过套接字来完成。
套接字调用流程如下图所示:
- socket():创建套接字。
- bind():指定本地地址。一个套接字用socket()创建后,它其实还没有与任何特定的本地或目的地址相关联。在很多情况下,应用程序并不关心它们使用的本地地址,这时就可以不用调用bind指定本地的地址,而由协议软件为它们选择一个。但是,在某个知名端口上操作的服务器进程必须要对系统指定本地端口。所以一旦创建了一个套接字,服务器就必须使用bind()系统调用为套接字建立一个本地地址。
- connect():将套接字连接到目的地址。初始创建的套接字并未与任何外地目的地址关联。客户机可以调用connect()为套接字绑定一个永久的目的地址,将它置于已连接状态。对数据流方式的套接字,必须在传输数据前,调用connect()构造一个与目的地的TCP连接,并在不能构造连接时返回一个差错代码。如果是数据报方式,则不是必须在传输数据前调用connect。如果调用了connect(),也并不像数据流方式那样发送请求建连的报文,而是只在本地存储目的地址,以后该socket上发送的所有数据都送往这个地址,程序员就可以免去为每一次发送数据都指定目的地址的麻烦。
- listen():设置等待连接状态。对于一个服务器的程序,当申请到套接字,并调用bind()与本地地址绑定后,就应该等待某个客户机的程序来要求连接。listen()就是把一个套接字设置为这种状态的函数。
- accept():接受连接请求。服务器进程使用系统调用socket,bind和listen创建一个套接字,将它绑定到知名的端口,并指定连接请求的队列长度。然后,服务器调用accept进入等待状态,直到到达一个连接请求。
- send()/recv()和sendto()/recvfrom():发送和接收数据 。在数据流方式中,一个连接建立以后,或者在数据报方式下,调用了connect()进行了套接字与目的地址的绑定后,就可以调用send()和reev()函数进行数据传输。
- closesocket():关闭套接字。
三、实例分析:跟踪分析 TCP 协议
实验三初始化 MenuOS 系统的网络功能中已经编译并且跟踪了sock_create函数和sock_map_fd函数。具体过程可参见上篇博客。
因为篇幅有限,下面对两个非常常用的系统调用sock_create函数和sock_map_fd函数进行跟踪分析。
1、sock_create函数
在~/linux-5.0.1/net/socket.c中找到了相应的函数定义,如下图(自己在里面加了一些注释):
这里比较重要的是sock_alloc()和pf->create()这两个函数。其中sock_alloc()里体现了linux一切皆文件理念,即使用文件系统来管理socket。
- sock_alloc()函数分配一个struct socket_alloc结构体,将sockfs相关属性填充在socket_alloc结构体的vfs_inode变量中,以限定后续对这个sock文件允许的操作。同时sock_alloc()最终返回socket_alloc结构体的socket变量,用于后续操作。
- pf->create调用了inet_create()函数,而inet_create()函数一方面通过inetsw[]数组获取对应协议类型的接口操作集信息,另一方面创建struct sock类型的变量,最后是对创建的sock进行初始化。
2、sock_map_fd()函数
在~/linux-5.0.1/net/socket.c中找到了相应的函数定义,如下图(自己在里面加了一些注释):
这个函数主要有两个部分,一个是创建file文件结构,fd文件描述符,另一部分是将file文件结构和fd文件描述符关联,同时将上一步返回的socket也一起绑定,形成一个完整的逻辑。
参考链接:
https://www.shiyanlou.com/courses/1198/learning/?id=9010
https://blog.csdn.net/u010039418/article/details/79347844