深入理解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编程系列函数的实现。