深入理解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編程系列函數的實現。