嵌入式Linux网络体系结构设计与TCP/IP协议栈(四)

本文详细介绍了嵌入式Linux网络体系结构中的网络层和传输层,包括IP协议的接收、前送、本地发送以及TCP/UDP协议的实现。在IP层,讲解了ip_rcv函数的分析、数据包过滤和路由决策过程。在传输层,重点阐述了UDP的发送和接收实现,包括连接初始化、数据报发送和接收的详细步骤。同时,概述了TCP的连接管理和套接字与IP层之间的接口。
摘要由CSDN通过智能技术生成

第7章 网络层传送

Internet协议的任务:

  1. 数据包校验和检验
  2. 防火墙对数据包过滤
  3. IP选项处理
  4. 数据分片和重组
  5. 接收、发送和前送

Internet协议头包含:Internet协议版本、IP协议头长度、服务类型(Tos)、数据包总长度、数据包标识符、数据包存活期、上层协议、校验和、源地址、目的地址、IP选项。

当数据包到达IP层后,sk_buff->data指针就已事先调整到Socket Buffer中存放IP协议头信息的起始地址处。IP层的协议处理函数就从Socket Buffer中解析出IP协议头信息,并放入iphdr类型的变量中,以便下一步协议处理函数按照IP协议头信息处理数据包

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u8	ihl:4,
		version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
	__u8	version:4,
  		ihl:4;							//IP协议头长度
#else
#error	"Please fix <asm/byteorder.h>"
#endif
	__u8	tos;						//服务类型
	__be16	tot_len;					//IP数据包总长度
	__be16	id;							//IP数据包标识符
	__be16	frag_off;					//分片数据在数据包中的偏移量
	__u8	ttl;						//数据包的生成周期
	__u8	protocol;					//上次协议
	__sum16	check;						//校验和
	__be32	saddr;						//数据包源地址
	__be32	daddr;						//数据包目标地址
	/*The options start here. */
};

PF_INET协议族初始化由inet_init函数实现,在完成协议族基本初始化之后,会调用协议族中各层协议的初始化函数建立起TCP/IP协议栈。主要功能为:

  • 注册协议栈各协议实例(TCP/UDP/ICMP等)
  • 初始化IP协议
  • 为套接字操作建立内存槽以便在打开套接字时为其分配所需的内存
  • 调用dev_add_pack注册IP数据包的接收处理函数ip_rcv,将处理函数插入到ptype_base中
  • 初始化AF_INET协议族在/proc文件系统的入口。

数据包到达之后需要对数据包做过滤检查才能决定是否对数据包做进一步处理——网络过滤子系统。分两个阶段:

  • do_something(如ip_rcv),只对数据包做必要的合法性检查,或为数据包预留需要的内存空间
  • do_something_finish(如ip_icv_finish),实际完成接收/发送操作的函数。

三个查询路由表的API:

  • ip_route_input : 确定输入包的目标地址
  • ip_route_output_flow : 返回网关地址和发送的网络设备
  • dst_pmtu: 返回最大传输单元

在IP层查询路由表时,路由表作出路由决策的主要依据是一下各数据域:

  • IP数据包的目标地址
  • IP数据包的源地址
  • 服务类型(Tos)
  • 接收数据包的网络设备
  • 可用于发送数据包的网络设备列表

7.1 ip_rcv函数分析

ip_rcv是典型的两阶段实现的函数,该函数主要是对数据包作正确性检查,然后调用网络过滤子系统的回调函数对数据包进行安全过滤,对数据包的实际处理功能大部分都在ip_rcv_finish函数中实现。
在这里插入图片描述

7.2 数据包前送

处理函数ip_forward
在这里插入图片描述

7.3 本地发送

在这里插入图片描述
ip_local_deliver_finish完成数据包从网络层向传输层发送
在IP协议头中protocol数据域占了8位,传输层最多可以有256个不同的协议。

enum {
  IPPROTO_IP = 0,		/* Dummy protocol for TCP		*/
#define IPPROTO_IP		IPPROTO_IP
  IPPROTO_ICMP = 1,		/* Internet Control Message Protocol	*/
#define IPPROTO_ICMP		IPPROTO_ICMP
  IPPROTO_IGMP = 2,		/* Internet Group Management Protocol	*/
#define IPPROTO_IGMP		IPPROTO_IGMP
  IPPROTO_IPIP = 4,		/* IPIP tunnels (older KA9Q tunnels use 94) */
#define IPPROTO_IPIP		IPPROTO_IPIP
  IPPROTO_TCP = 6,		/* Transmission Control Protocol	*/
#define IPPROTO_TCP		IPPROTO_TCP
...
IPPROTO_RAW = 255,		/* Raw IP packets			*/

内核中所有注册了传输层协议处理函数实例,存放在一个struct net_protocol 类型的inet_protps全局数组中,形成向量表,网络层协议头中的protocol数据域描述的协议编码就是该协议的struct net_protocol 实例在inet_protos向量表中的索引。

struct net_protocol {
	int			(*early_demux)(struct sk_buff *skb);
	int			(*early_demux_handler)(struct sk_buff *skb);
	int			(*handler)(struct sk_buff *skb);
	void			(*err_handler)(struct sk_buff *skb, u32 info);
	unsigned int		no_policy:1,
				netns_ok:1,
				/* does the protocol do more stringent
				 * icmp tag validation than simple
				 * socket lookup?
				 */
				icmp_strict_tag_validation:1;
};
extern struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS];

在这里插入图片描述
ip_local_deliver_finish函数的正式处理部分从获取访问inet_protos向量表的读保护锁rcu_read_lock开始,依次完成以下过程:

  1. 从IP头获取传输层协议的编码
  2. 如果该协议是裸套接字处理函数,则将数据包传给裸套接字处理函数
  3. 以iphdr->protocol数据域为索引在inet_protocols向量表中查询,找到对应的传输层协议处理函数结构块
    ip_local_deliver_finish完成后,数据包就离开网络层上传至TCP/IP协议栈的传输层

7.4 在IP层的发送

函数名功能描述
ip_queue_xmit由传输层的TCP协议调用,将数据包从传输层发送给网络层,创建协议头和IP选项到数据包中,调用dst_output发送
ip_append_data由传输层的UDP等协议调用,缓存从传输层传送到网络层的请求发送数据包缓冲区
ip_append_page由传输层的UDP等协议调用,缓存从传输层传送到网络层的请求发送数据页面
ip_push_pending_frams将ip_append_data和ip_append_page创建的输出队列发送出去
dst_output数据包发送函数,当数据包的目标地址是其他主机时,初始化为ip_output
ip_build_and_send_pkt用于TCP发送同步回答消息时创建IP协议头的选项并发送
ip_send_reply用于TCP发送回答消息和复位时创建IP协议头和选项并发送数据包
数据结构描述
struct sock套接字数据结构
struct inet_sockPF_INET特定协议族套接字结构
struct ipcm_cookie包含发送数据包需要的各种信息的数据结构
struct cork嵌套在inet_sock数据结构中,处理套接字阻塞时的选项信息
struct dst_entry路由表入口
struct rtable路由表结构
struct in_device存放网络设备的所有与IPv4协议相关的配置
struct in_ifaddr如果为一个网络接口分配了IPv4网络地址,该数据结构用于存放IP地址和与地址配置相关信息

在这里插入图片描述
操作struct sock、struct inet_sock数据结构的关键函数

  • sk_dst_set和_sk_dst_set 保存到达目的地址使用的路由
  • sk_dst_check和_sk_dst_check 测试到达目标地址的路由是否有效
  • skb_set_owner_w 指定数据包所属的套接字
  • sock_alloc_send_skb: 分配单个缓冲区或一系列分配数据包的第一个缓冲区;
    sock_wmalloc : 管理其余子分片。

ip_queue_xmit 由TCP和SCTP协议调用,处理本地产生的外送数据包,是传输层向网络层传输数据包时调用的函数。

  • 设置路由:ip_route_output_flow寻找下一条的新路由
  • 构建IP协议头
  • NET_INET_LOCAL_OUT
  • dst_output

在这里插入图片描述
ip_append_data 不传送数据,而是把数据放到一个大小适中的缓冲区中,随后的函数对数据包分段处理,并将数据包发送出去。

当数据发送请求来自用户地址空间,应用程序调用sndmsg来请求将数据从用户空间移动到内核地址空间时,这个复制是由ip_append_data函数的输入参数getfrag函数来完成,传输层对应的getfrag例程如下:

协议数据复制函数
ICMPicmp_glue_bits
UDPip_generic_getfrag
裸IPip_generic_getfrag
TCPip_reply_glue_bits

ip_append_page :将frag数组初始化指向接收数据缓冲区的位置,在必要的时候计算传输层的校验和。
只有在网络设备支持Scatter/Gather I/O功能时才能使用。

在这里插入图片描述

第8章 传输层UDP协议的实现

UDP是不可靠、无连接的数据报协议。“不可靠”仅仅意味着在UDP协议中没有检测数据是否能够到达网络另一端的机制,在主机内,UDP可以保证正确的传递数据。

使用UDP传输数据的原因:

  1. 数据量很小,创建连接的开销、保证可靠发送需要做的工作可能比发送数据本身的工作量还大
  2. 应用程序本身保证可靠发送机制
  3. 面向业务的应用程序(DNS),只有一个请求和一个应答,建立连接和维护连接的开销太大
    在这里插入图片描述

8.1 UDP协议实现的关键数据结构

//UDP协议头信息,include/linux/udp.h
struct udphdr{
    __be16 source;//源端口
    __be16 dest;//目的端口
    __be16 len;//UDP数据包长度
    __sum16 check;//校验和
}
//UDP控制缓冲区(sk_buff中存放各层协议的私有数据) //include/net/udp.h
struct udp_skb_cb{
    union{  // IPv4和IPv6选项信息
        struct inet_skb_parm h4;
#if defined (CONFIG_IPV6) || defined (CONFIG_IPV6_MOUDLE)
        struct inet6_skb_parm h6;
#endif
    }header;
    __u16 cscov;	//校验和覆盖的UDP数据包长度
    __u8 partial_cov;	//指明UDP计算部分校验和以及校验和覆盖的UDP数据包长度
};
//只能通过如下宏访问
#define UDP_SKB_CB(__skb) (struct udp_skb_cb *)((__skb)->cb)

//UDP套接字
struct udp_sock{
    struct inet_sock inet;  //PF_INET特定协议族套接字结构 -> struct sock sk(套接字)
    int pending;	//是否有等待发送(悬挂)的数据包
    unsigned int corkflag;	//是否需要暂时阻塞套接字
    __u16 encap_type;	//是否为封装的套接字
    __u16 len;		//等待发送的数据包的总长度
    __u16 pcslen;	//等待发送数据包长度
    __u16 pcrlen;	//等待接受数据包长度
    __u8 pcflag;	//是否为轻套接字
    __u8 unused[3];	
    int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);//UDP套接字的接收函数
};

与套接字层之间的接口:

struct proto udp_proto={
    .name = "UDP",
    .owner = THIS_MOUDLE,
    .close = udp_lib_close,
    .connect = ip4_datagrame_connect,
    .disconnect = udp_disconnect,
    .ioctl = udp_ioctl,
    .destory = udp_destroy_sock,
    .setsockopt = udp_setsockopt,
    .getsockopt = udp_getsockopt,
    .sendmsg = udp_sendmsg,
    .recvmsg = udp_recvmsg,
    .backlog_rcv = __udp_queue_rcv_skb,
    .hash = udp_lib_hash,
    .unhash = udp_lib_unhash,
    .get_port = udp_v4_get_port,
    ...
}

UDP实例由proto_register函数在 inet_init 中调用注册。
与IP层的接口:

static  struct net_protocol udp_protocol = {
	.handler =	udp_rcv,
	.err_handler =	udp_err,
	.no_policy =	1,
	.netns_ok =	1,
};

在inet_init 中调用inet_add_protocol(&udp_protocol, IPPROTO_UDP) 注册。

8.2 发送UDP数据报的实现

用户程序在应用层调用socket系统调用,打开一个SOCK_DGRAM类型的套接字,在通过该套接字接收和发送数据时,数据包会通过UDP协议穿过TCP/IP协议栈向外发送,或由UDP协议上传给套接字到达应用程序。

8.2.1 初始化一个连接

UDP套接字调用connect : 建立到达目标地址的路由,并把该路由放入路由高速缓冲存储器中。一旦路由建立起来,接下来在通过UDP套接字发送数据包时就可以使用路由高速缓冲区中的信息了。这种方式称之为在连接套接字上的快速路径"fast path"。

SOCK_DGRAM类套接字调用connect系统调用时,套接字层会转而调用ip4_datagram_connect 函数

int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
// sk : 指向打开的UDP套接字的struct sock数据结构
//   uaddr : 目标地址
//	addr_len : 目标地址长度
    
    //对建立连接的信息做正确性检查:地址长度、协议类型
	if (addr_len < sizeof(*usin))
		return -EINVAL;

	if (usin->sin_family != AF_INET)
		return -EAFNOSUPPORT;
	//复位套接字中源目标地址在路由缓存中的记录
	sk_dst_reset(sk);
	//套接字为与网络接口绑定的套接字,将绑定的网络接口信息保存到oif局部变量中
	oif = sk->sk_bound_dev_if;
	saddr = inet->inet_saddr;
	//如果建立连接的地址是组传送地址,则重新初始化oif与源地址saddr
	if (ipv4_is_multicast(usin->sin_addr.s_addr)) {
		if (!oif)
			oif = inet->mc_index;
		if (!saddr)
			saddr = inet->mc_addr;
	}
	fl4 = &inet->cork.fl.u.ip4;
	//为连接寻址一个新路由,如果寻址新路由成功,则将新路由放入缓存
	rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr,
			      RT_CONN_FLAGS(sk), oif,
			      sk->sk_protocol,
			      inet->inet_sport, usin->sin_port, sk);
	//如果寻址新路由不成功,则更新错误统计信息,返回错误代码
	if (IS_ERR(rt)) {
		err = PTR_ERR(rt);
		if (err == -ENETUNREACH)
			IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
		goto out;
	}
	//如果寻址的新路由为广播地址路由,则释放该路由在路由缓存中的入口,返回错误代码
	if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) {
		ip_rt_put(rt);
		err = -EACCES;
		goto out;
	}
	//用从路由表中获取的信息更新UDP连接的源地址和目标地址
	if (!inet->inet_saddr)
		inet->inet_saddr = fl4->saddr;	/* Update source address */
	if (!inet->inet_rcv_saddr) {
		inet->inet_rcv_saddr = fl4->saddr;
		if (sk->sk_prot->rehash)
			sk->sk_prot->rehash(sk);
	}
	inet->inet_daddr = fl4->daddr;
	//目标端口号来自用户程序,设置套接字状态为TCP_ESTABLISHED
	inet->inet_dport = usin->sin_port;
	sk->sk_state = TCP_ESTABLISHED;
	sk_set_txhash(sk);
	inet->inet_id = jiffies;
	//新路由在路由高速缓存中的入口保存于套接字sk->sk_dst_cache数据域
	sk_dst_set(sk, &rt->dst);

套接字状态设置为TCP_ESTABLISHED,说明目标路由已缓存在路由高速缓冲区中,当用户程序发送数据包时,如果没有给出数据包的目标地址,数据包仍可发送,因为套接字的状态指明路由已建立。

8.2.2 发送数据包

udp_sendmsg 功能是从用户地址空间接收发送数据,复制到内核地址空间;通过有效的路由向外发送。

int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
//sk : 套接字
// msg : 存放和管理来自用户地址空间的数据
// len : 从用户地址空间复制数据的总长度
  1. 套接字中有挂起的数据等待发送,将数据帧加入到IP层缓冲区
  2. 从入参获取目标地址和端口号,若获取不到,则查看套接字是否已连接
  3. 设置控制信息,消息体中查看,没有则从inet选项数据域中提取
  4. 如果数据包在本地局域网中传:SOCK_LOCALROUTE,msg_flags标志为不需要路由或IP选项设置了严格源路由,则不需要寻址数据包路由;目标IP地址是组传送地址,则不需要寻址数据包路由
  5. 如无有效路由,调用ip_route_output_flow在路由表中寻址新路由
  6. 向IP层传送UDP数据报

8.3 UDP协议接收的实现

传输层的AF_INET协议族协议实例(TCP/UDP)从网络层IP处接收数据包,由IP协议头中的protocol数据域得出具体由哪个协议接收该数据包,查询哈希链表inet_protos[MAX_INET_PROTOS]确定接收函数。

在inet_init中调用inet_add_protocol函数注册数据包接收处理函数到inet_protocol[MAX_INET_PROTOCOL]全局哈希链表。

UDP协议接收处理函数为udp_rcv -> __udp4_lib_rcv,主要功能:正确性检查、地址类型分析(唯一主机地址、组发送或广播地址),调用相应的函数处理过程。其中,用户进程由与UDP协议头中目标端口号相匹配的套接字给出,每个打开的套接字都保存在UDP哈希表中(udp_table)。

//入参为输入的数据包skb,UDP哈希表udp_table
__udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
  1. 当输入包的发送地址是广播地址或组发送地址时,调用__udp4_lib_mcast_deliver函数完成发送过程
  2. 确认是否有打开的套接字等待接收数据包
    a)有打开的套接字再等待接受数据,udp_queue_rcv_skb函数发送给套接字的接收缓冲区队列,接收处理过程完成。
    b) 没有打开的套接字,则校验和检验,正确则更新错误统计信息,向数据包发送端返回ICMP错误信息,告知端口不可达,释放Socket Buffer。

8.3.1 将数据包放入套接字接收队列的处理函数

当套接字为常规套接字时,接收函数执行的关键步骤为:

  1. 锁定套接字
  2. 获取等待接收数据包的用户进程
  3. 数据包放入套接字接收队列

如果udp_queue_rcv_skb获取用户进程成功,则数据包就由__udp_queue_rcv_skb函数套接字的接收缓冲区队列。
如果udp_queue_rcv_skb没有发现等待接收数据包的用户进程,则将数据包放入套接字的backlog队列,等待以后的套接字接收。

组发送和广播数据包会传给多个目标地址,在同一个主机上就可能有多个目标端口在等待接收数据包。当UDP协议实例处理组发送或广播数据包时,协议接收函数查看是否有多个打开的套接字要接收数据包。当输入数据包的路由入口标志设置了组发送或广播发送标志时,UDP的接收函数__udp4_lib_rcv就调用UDP协议的组接收函数__udp4_lib_mcast_deliver,将数据包分发给所有有效的侦听套接字,函数处理流程需要遍历UDP哈希链表,找到所有接收数据包的套接字。

8.3.2 UDP的哈希链表

哈希表结构体

struct udp_table {
	struct udp_hslot	*hash;
	struct udp_hslot	*hash2;
	unsigned int		mask;
	unsigned int		log;
};
extern struct udp_table udp_table;

初始化在 udp_init -> udp_table_init ,哈希表中搜索的索引值是由UDP端口号的低7位计算出来的。

#用来把参数转换成字符串
##运算符可以用于宏函数的替换部分。这个运算符把两个语言符号组合成单个语言符号,为宏扩展提供了一种连接实际变元的手段 x ## n => xn
VA_ARGS 是一个可变参数的宏
##VA_ARGS 宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉的作用,否则会编译出错

第9章 传输层TCP协议的实现

TCP提供可靠、面向连接、字节流传送服务。
面向连接:在应用TCP协议进行通信之前双方通常需要通过三次握手来建立TCP连接,连接建立后才能进行正常的数据传输,因此广播和多播不会承载在TCP协议上。
可靠性:由于TCP处于多跳通信的IP层之上,而IP层并不提供可靠的传输,因此在TCP层看来就有四种常见传输错误问题,分别是比特错误(packet bit errors)、包乱序(packet reordering)、包重复(packet duplication)、丢包(packet erasure或称为packet drops),因此TCP要提供可靠的传输,就需要具有超时与重传管理、窗口管理、流量控制、拥塞控制等功能。
字节流式:维护字节流传送顺序,报头中的序列号和回答序列号用于跟踪字节传送顺序。

TCP报文格式
CP封装在IP报文中的时候,如下图所示,TCP头紧接着IP头(IPV6有扩展头的时候,则TCP头在扩展头后面),不携带选项(option)的TCP头长为20bytes,携带选项的TCP头最长可到60bytes。
在这里插入图片描述
其中header length字段由4比特构成,最大值为15,单位是32比特,即头长的最大值为15*32 bits = 60bytes,因此上面说携带选项的TCP头长最长为60bytes。
在这里插入图片描述
TCP的源端口、目的端口、以及IP层的源IP地址、目的IP地址四元组唯一的标识了一个TCP连接
TCP各字段释义
TCP源端口(Source Port):16位的源端口其中包含发送方应用程序对应的端口。源端口和源IP地址标示报文发送端的地址。

TCP目的端口(Destination port):16位的目的端口域定义传输的目的。这个端口指明报文接收计算机上的应用程序地址接口。

TCP序列号(SequenceNumber):32位的序列号标识了TCP报文中第一个byte在对应方向的传输中对应的字节序号。当SYN出现,SN=ISN(随机值)单位是byte。比如发送端发送的一个TCP包净荷(不包含TCP头)为12byte,SN为5,则发送端接着发送的下一个数据包的时候,SN应该设置为5+12=17。通过序列号,TCP接收端可以识别出重复接收到的TCP包,从而丢弃重复包,同时对于乱序数据包也可以依靠系列号进行重排序,进而对高层提供有序的数据流。另外如果接收的包中包含SYN或FIN标志位,逻辑上也占用1个byte,应答号需加1。

TCP应答号(Acknowledgment Number简称ACK Number):32位的ACK Number标识了报文发送端期望接收的字节序列。如果设置了ACK控制位,这个值表示一个准备接收的包的序列码,注意是准备接收的包,比如当前接收端接收到一个净荷为12byte的数据包,SN为5,则会回复一个确认收到的数据包,如果这个数据包之前的数据也都已经收到了,这个数据包中的ACK Number则设置为12+5=17,表示之前的数据都已经收到了,准备接受SN=17的数据包。
头长(Header Length):4位包括TCP头大小,指示TCP头的长度,即数据从何处开始。

保留(Reserved):4位值域,这些位必须是0。为了将来定义新的用途所保留,其中RFC3540将Reserved字段中的最后一位定义为Nonce标志。后续拥塞控制部分的讲解我们会简单介绍Nonce标志位。

标志(Code Bits):8位标志位

  1. CWR(Congestion Window Reduce):拥塞窗口减少标志set by sender,用来表明它接收到了设置ECE标志的TCP包。并且sender 在收到消息之后已经通过降低发送窗口的大小来降低发送速率。
  2. ECE(ECN Echo):ECN响应标志被用来在TCP3次握手时表明一个TCP端是具备ECN功能的。在数据传输过程中也用来表明接收到的TCP包的IP头部的ECN被设置为11。注:IP头部的ECN被设置为11表明网络线路拥堵。
  3. URG(Urgent):该标志位置位表示紧急(The urgent pointer) 标志有效。该标志位目前已经很少使用参考后面流量控制和窗口管理部分的介绍。
  4. ACK:取值1代表Acknowledgment Number字段有效,这是一个确认的TCP包,取值0则不是确认包
  5. PSH(Push):该标志置位时,一般是表示发送端缓存中已经没有待发送的数据,接收端不将该数据进行队列处理,而是尽可能快将数据转由应用处理。在处理 telnet 或 rlogin 等交互模式的连接时,该标志总是置位的。
  6. RST(Reset):用于reset相应的TCP连接。通常在发生异常或者错误的时候会触发复位TCP连接。
  7. SYN:同步序列编号(Synchronize Sequence Numbers)有效。该标志仅在三次握手建立TCP连接时有效。
  8. FIN(Finish):No more data from sender。当FIN标志有效的时候我们称呼这个包为FIN包。

窗口大小(Window Size):16位,该值指示了从Ack Number开始还愿意接收多少byte的数据量,也即用来表示当前接收端的接收窗还有多少剩余空间,用于TCP的流量控制。

校验位(Checksum):16位TCP头。发送端基于数据内容计算一个数值,接收端要与发送端数值结果完全一样,才能证明数据的有效性。接收端checksum校验失败的时候会直接丢掉这个数据包。CheckSum是根据伪头+TCP头+TCP数据三部分进行计算的。

优先指针(紧急,Urgent Pointer):16位,指向后面是优先数据的字节,在URG标志设置了时才有效。如果URG标志没有被设置,紧急域作为填充。

选项(Option):长度不定,但长度必须以是32bits的整数倍。常见的选项包括MSS、SACK、Timestamp等等。

TCP协议头数据结构:

struct tcphdr {
	__be16	source;
	__be16	dest;
	__be32	seq;
	__be32	ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u16	res1:4,
		doff:4,	//协议头长度
		fin:1,
		syn:1,
		rst:1,
		psh:1,
		ack:1,
		urg:1,
		ece:1,//网络阻塞
		cwr:1;//网络窗口
#elif defined(__BIG_ENDIAN_BITFIELD)
	__u16	doff:4,
		res1:4,
		cwr:1,
		ece:1,
		urg:1,
		ack:1,
		psh:1,
		rst:1,
		syn:1,
		fin:1;
#else
#error	"Adjust your <asm/byteorder.h> defines"
#endif	
	__be16	window;
	__sum16	check;
	__be16	urg_ptr;
};

TCP控制缓冲区:发送数据时,TCP层分配Socket Buffer来存放写入套接字的数据,控制管理数据包的信息存放在TCP的控制缓冲区中。

struct tcp_skb_cb {
	__u32		seq;		/* Starting sequence number	*/ //输出数据段的起始序列号
	__u32		end_seq;	/* SEQ + FIN + SYN + datalen	*/ //最后一个输出数据段结束序列号
	union {
		/* Note : tcp_tw_isn is used in input path only
		 *	  (isn chosen by tcp_timewait_state_process())
		 *
		 * 	  tcp_gso_segs/size are used in write queue only,
		 *	  cf tcp_skb_pcount()/tcp_skb_mss()
		 */
		__u32		tcp_tw_isn;
		struct {
			u16	tcp_gso_segs;
			u16	tcp_gso_size;
		};
	};
	__u8		tcp_flags;	/* TCP header flags. (tcp[13])	*/

	__u8		sacked;		/* State flags for SACK.	*/
#define TCPCB_SACKED_ACKED	0x01	/* SKB ACK'd by a SACK block	*/
#define TCPCB_SACKED_RETRANS	0x02	/* SKB retransmitted		*/
#define TCPCB_LOST		0x04	/* SKB is lost			*/
#define TCPCB_TAGBITS		0x07	/* All tag bits			*/
#define TCPCB_REPAIRED		0x10	/* SKB repaired (no skb_mstamp)	*/
#define TCPCB_EVER_RETRANS	0x80	/* Ever retransmitted frame	*/
#define TCPCB_RETRANS		(TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS| \
				TCPCB_REPAIRED)

	__u8		ip_dsfield;	/* IPv4 tos or IPv6 dsfield	*/
	__u8		txstamp_ack:1,	/* Record TX timestamp for ack? */
			eor:1,		/* Is skb MSG_EOR marked? */
			has_rxtstamp:1,	/* SKB has a RX timestamp	*/
			unused:5;
	__u32		ack_seq;	/* Sequence number ACK'd	*/
	union {
		struct {
			/* There is space for up to 24 bytes */
			__u32 in_flight:30,/* Bytes in flight at transmit */
			      is_app_limited:1, /* cwnd not fully used? */
			      unused:1;
			/* pkts S/ACKed so far upon tx of skb, incl retrans: */
			__u32 delivered;
			/* start of send pipeline phase */
			u64 first_tx_mstamp;
			/* when we reached the "delivered" count */
			u64 delivered_mstamp;
		} tx;   /* only used for outgoing skbs */
		union {
			struct inet_skb_parm	h4; //输入数据段的IP选项
#if IS_ENABLED(CONFIG_IPV6)
			struct inet6_skb_parm	h6;
#endif
		} header;	/* For incoming skbs */
		struct {
			__u32 flags;
			struct sock *sk_redir;
			void *data_end;
		} bpf;
	};
};

TCP套接字数据结构: struct tcp_sock (include/linux/tcp.h),包含了TCP层管理数据传送需要的所有信息。

应用层传给传输层信息的数据结构:
struct msghdr {
void msg_name; / 套接字的名字 /
int msg_namelen; /
套接字的长度 /
struct iov_iter msg_iter; /
data */
void msg_control; / ancillary data /
__kernel_size_t msg_controllen; /
控制描述链表长度 /
unsigned int msg_flags; /
flags on received message */
struct kiocb msg_iocb; / 控制标志 */
};

9.1 套接字、TCP协议和IP层之间的接口

1.套接字与TCP之间的接口

struct proto tcp_prot = { //初始化套接字与传输层之间的接口
	.name			= "TCP",
	.owner			= THIS_MODULE,
	.close			= tcp_close,
	.pre_connect		= tcp_v4_pre_connect,
	.connect		= tcp_v4_connect,
	.disconnect		= tcp_disconnect,
	.accept			= inet_csk_accept,
	.ioctl			= tcp_ioctl,
	.init			= tcp_v4_init_sock,
	...
	.recvmsg		= tcp_recvmsg,
	.sendmsg		= tcp_sendmsg,
	...

通过rc = proto_register(&tcp_prot, 1);注册

2.TCP与IP之间的接收接口

static struct net_protocol tcp_protocol = {	//IP层与传输层数据包接收接口
	.early_demux	=	tcp_v4_early_demux,
	.early_demux_handler =  tcp_v4_early_demux,
	.handler	=	tcp_v4_rcv,
	.err_handler	=	tcp_v4_err,
	.no_policy	=	1,
	.netns_ok	=	1,
	.icmp_strict_tag_validation = 1,
};

通过if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)注册

3.TCP与IP之间的发送接口

const struct inet_connection_sock_af_ops ipv4_specific = {  //tcp与ip之间的发送接口
	.queue_xmit	   = ip_queue_xmit,//ipv4网络层传送函数
	.send_check	   = tcp_v4_send_check,//计算TCP发送数据段校验和函数
	.rebuild_header	   = inet_sk_rebuild_header,//创建TCP协议头
	.sk_rx_dst_set	   = inet_sk_rx_dst_set,//
	.conn_request	   = tcp_v4_conn_request,//处理连接请求数据段
	.syn_recv_sock	   = tcp_v4_syn_recv_sock,//从另一端点收到SYNACK回答后创建新的子套接字函数
	.net_header_len	   = sizeof(struct iphdr),//网络层协议头的大小,设置为IPv4协议头长度
	.setsockopt	   = ip_setsockopt,//设置IPv4在网络层的套接字选项
	.getsockopt	   = ip_getsockopt,//获取IPv4在网络层的套接字选项
	.addr2sockaddr	   = inet_csk_addr2sockaddr,//为IPv4生成常规sockaddr_in类型地址
	.sockaddr_len	   = sizeof(struct sockaddr_in),//IPv4的sockaddr_in类型地址大小
#ifdef CONFIG_COMPAT
	.compat_setsockopt = compat_ip_setsockopt,
	.compat_getsockopt = compat_ip_getsockopt,
#endif
	.mtu_reduced	   = tcp_v4_mtu_reduced,
};

9.2 TCP套接字的连接管理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • TCP 使用的套接字类型为SOCK_STREAM,创建套接字:
int sd;
sd = sock(AF_INET, SOCK_STREAM, NULL);//sd为套接字描述符,后续通过套接字描述符访问套接字
  • 建立连接需要的信息包括服务器的IP地址、端口号、地址类型等:
struct scokadder_in{
	sa_family_t sin_family; /*地址族*/
	in_port_t sin_port; /*端口号*/
	struct in_addr sin_addr; /*IPv4的IP地址*/
}
//初始化赋值;
struct sockaddr_in daddr;
daddr.sin_family = AF_INET;
daddr.sin_sin_addr.s_addr = htonl(目的IP);
daddr.sin_port = htons(目的端口);
  • 设置TCP选项 setsockopt ,最终传给tcp_v4_connect
  • 发出连接请求,执行套接字层连接请求函数为tcp_v4_connect
connect(sd,(struct sockaddr *)&daddr, sizeof(struct sockaddr));
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值