千家信息网

如何深入理解TCP/IP协议的socket实现

发表于:2024-11-29 作者:千家信息网编辑
千家信息网最后更新 2024年11月29日,这篇文章给大家介绍如何深入理解TCP/IP协议的socket实现,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。socket大家都知道是用于网络通信的,也知道他是ip和端口的组合。
千家信息网最后更新 2024年11月29日如何深入理解TCP/IP协议的socket实现

这篇文章给大家介绍如何深入理解TCP/IP协议的socket实现,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

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函数集。

关于如何深入理解TCP/IP协议的socket实现就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

0