深入理解TCP/IP协议的实现之socket(基于linux1.2.13)
- 2020 年 3 月 12 日
- 笔记
socket大家都知道是用于网络通信的,也知道他是ip和端口的组合。但是很多同学可能不是很清楚socket的原理和实现。下面我们深入理解一下socket到底是什么。 我们回忆一下socket编程的步骤,不管是客户端还是服务端,第一个调的函数都是socket。我们就从这个函数的实现开始,看看一个socket到底是什么。
// 新建一个socket结构体,并且创建一个下层的sock结构体,互相关联 static int sock_socket(int family, int type, int protocol) { int i, fd; struct socket *sock; struct proto_ops *ops; // 找到对应的协议族,比如unix域、ipv4 for (i = 0; i < NPROTO; ++i) { // 从props数组中找到family协议对应的操作函数集,props由系统初始化时sock_register进行操作 if (pops[i] == NULL) continue; if (pops[i]->family == family) break; } if (i == NPROTO) { return -EINVAL; } // 函数集 ops = pops[i]; // 检查一下类型 if ((type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_SEQPACKET && type != SOCK_RAW && type != SOCK_PACKET) || protocol < 0) return(-EINVAL); // 分配一个新的socket结构体 if (!(sock = sock_alloc())) { ... } // 设置类型和操作函数集 sock->type = type; sock->ops = ops; if ((i = sock->ops->create(sock, protocol)) < 0) { sock_release(sock); return(i); } // 返回一个新的文件描述符 if ((fd = get_fd(SOCK_INODE(sock))) < 0) { sock_release(sock); return(-EINVAL); } return(fd); }
我们从上到下,逐步分析这个过程。 1. 根据传的协议类型,找到对应的函数集,因为不同的协议族他的底层操作是不一样的。 2. 分配一个socket结构体。定义如下。我们大概了解一下字段就行。
struct socket { short type; /* SOCK_STREAM, ... */ socket_state state; long flags; struct proto_ops *ops; // 这个字段要记一下 void *data; struct socket *conn; struct socket *iconn; struct socket *next; struct wait_queue **wait; struct inode *inode; struct fasync_struct *fasync_list; }; struct socket *sock_alloc(void) { struct inode * inode; struct socket * sock; // 获取一个可用的inode节点 inode = get_empty_inode(); if (!inode) return NULL; // 初始化某些字段 inode->i_mode = S_IFSOCK; inode->i_sock = 1;// socket文件 inode->i_uid = current->uid; inode->i_gid = current->gid; // 指向inode的socket结构体,初始化inode结构体的socket结构体 sock = &inode->u.socket_i; sock->state = SS_UNCONNECTED; sock->flags = 0; sock->ops = NULL; sock->data = NULL; sock->conn = NULL; sock->iconn = NULL; sock->next = NULL; sock->wait = &inode->i_wait; // 互相引用 sock->inode = inode; /* "backlink": we could use pointer arithmetic instead */ sock->fasync_list = NULL; // socket数加一 sockets_in_use++; // 返回新的socket结构体,他挂载在inode中 return sock; }
sock_alloc首先分配了一个inode,inode节点里有一个socket结构体,然后初始化socket结构体的一些字段,并把他的地址返回。
3. 这时候我们拿到一个socket结构体。接着调create函数(省略了部分代码)。
// 创建一个sock结构体,和socket结构体互相关联 static int inet_create(struct socket *sock, int protocol) { struct sock *sk; struct proto *prot; int err; // 分配一个sock结构体 sk = (struct sock *) kmalloc(sizeof(*sk), GFP_KERNEL); switch(sock->type) { case SOCK_STREAM: protocol = IPPROTO_TCP; // 函数集 prot = &tcp_prot; break; case SOCK_DGRAM: protocol = IPPROTO_UDP; prot=&udp_prot; break; } // sock结构体的socket字段指向上层的socket结构体 sk->socket = sock; // 省略一堆对sock结构体的初始化代码 }
我们发现创建一个socket的时候,申请了一个socket结构体,同时也申请了一个sock结构体。为什么需要两个结构体,并且这两个结构体关联在一起呢?这要说到网络协议的复杂性,而这个设计就是linux对这个复杂性的解决方案。我们回头看看socket函数的参数。
socket(int family, int type, int protocol)
family是协议簇,比如unix域、ipv4、ipv6,type是在第一个参数的基础上的子分类。比如ipv4下有tcp、udp、raw、packet。protocol对tcp、udp没用,对raw、packet的话是标记上层协议类型。这好比一棵树一样,从根节点开始,有很多分支。socket结构体是整个网络协议实现的最上层结构,是第一层抽象。根据协议簇的不同,有不同的实现函数,在同一协议簇下,也有不同的子分类,比如ipv4下有tcp、udp等。不同子类具体的逻辑也不一样。即数据结构和算法都不一样。所以socket结构体有一个data字段,他是自定义的,对于ipv4的实现,他是指向一个sock结构体,对于unix域的实现,unix_proto_data结构体。这就解决了不同协议簇(family)不同实现的问题。那对于同一协议簇下的不同子类型,又如何实现呢?比如ipv4下的tcp、udp。linux给出的方案是在sock结构体中定义一个字段,根据子类型type的值,指向不同的底层协议函数集。

在申请完sock结构体并且和socket结构体互相关联后。这时候我们拿到了一个inode,一个socket结构体,一个sock结构体。然后根据inode拿到一个file和fd文件描述符。最后返回fd给用户。内容结构图如下。

这就是socket函数返回后的内存结构体。后续我们调用bind,listen等等函数,传入fd,系统就会根据上面图的指向,一直找到tcp函数集,执行对应的函数,对于udp也是一样,不同是tcp函数集变成udp函数集。这一篇我们先介绍socket函数的逻辑,下面继续分析socket编程系列函数的实现。