第13讲 | 套接字Socket:Talk is cheap, show me the code

这一节我们讲基于TCP和UDP协议的Socket编程。

TCP和UDP协议分客户端和服务端,Socket编程也同样这么分。

Socket,这个单词的含义有插口或者插槽的意思。你可以想象网络通信就是弄一根网线,一头插在客户端,一头插在服务端,然后进行通信。所以通信前双方首先都要建立一个Socket。

建立Socket的时候,要设置哪些参数呢?Socket编程进行的是端到端的网络通信,它不知道中间经过的网络设备,因而能够设置的参数,只能是端到端协议上的网络层和传输层。

在网络层,Socket函数需要指定到底是IPv4还是IPv6,分别对应设置为AF_INET和AF_INET6。还要指定是TCP还是UDP,分别是SOCK_STREAM和SOCK_DGRAM。

基于TCP协议的Socket程序函数调用过程

两端创建了Socket之后,接下来的过程TCP和UDP稍有不同,我们先看TCP。

TCP的服务端首先监听一个端口,一般是调用bind函数,给创建的socket赋予一个IP和端口。为什么需要端口?你写的是一个应用程序,当一个网络包来的时候,内核需要通过TCP头的目的端口号,找到你这个应用程序,把包给你。为什么需要IP地址呢?一台机器可能有多个网卡,每个网卡都有自己的IP地址,你可以选择监听所有的网卡,也可以监听某一个网卡,这样,只有给这个网卡的包才会给你。

然后,就可以调用listen函数进行监听。当调用了这个函数之后,服务端就进入了listen状态,这个时候客户端就可以发起连接了。

在内核中,为每个Socket维护两个队列。一个是已经建立了连接的队列,这个时候连接已经经过三次握手,处于establised状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于syn_rcvd状态。

接下来,服务端调用accept函数,拿出一个已经建立好的连接进行处理。如果还没有完成,就要等着。

在服务端等待的时候,客户端通过调用connect函数发起连接,先在参数中指明要连接的IP地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept会返回另一个Socket。

这里需要区分,监听的Socket和真正用来传数据的Socket是两个,一个叫做监听Socket,一个叫做已连接Scoket。

连接建立成功之后,双方通过read和write函数读写数据,就像往一个文件流里写东西。

下面这个图是基于TCP协议的Socket程序函数调用过程。

在内核中,Socket是一个文件,对应就有文件描述符。每个进程都有一个数据结构task_struct,里面指向一个文件描述符数组,列出这个进程打开的所有文件的文件描述符。文件描述符是一个整数,是这个数组的下标。

这个数组中的内容是一个指针,指向内核中所有打开的文件的列表。既然是文件,就会有inode,socket对应的inode不是保存在硬盘上,而是保存在内存中。这个inode中,指向了Socket在内核中的Socket结构。

在这个结构里,主要是两个队列,一个是发送队列,一个是结构队列。这两个队列里保存的是一个缓存sk_buff。这个缓存里能够看到完整的包的结构。

基于UDP协议的Socket程序函数调用过程

UDP和TCP有些不一样,UDP是没有连接的,所以不需要三次握手,也就不需要调用listen和connect,但是UDP依然需要IP和端口号,所以需要bind。UDP不维护连接状态,因而不需要每队连接建立一组Socket,只要有一个Socket,就能够和多个客户端通信。每次通信的时候调用sendto和recvfrom,都可以传入IP地址和端口。

下图就是基于UDP协议的Socket程序函数调用过程。

服务器如如何接更多项目?

了解这个socket函数之后,就可以轻松写一个网络交互的程序了。网络建立之后,进行一个死循环。客户端发了收,收了发。

但是这种方法只能进行一对一的沟通。如果是一个服务器,肯定要服务多个客户端。就好像老板成立一个公司,所有活自己一个人干,肯定是不行的。

作为老板肯定要想,我要接的项目越多越好。

我们先算一下最大连接数的理论值。系统会用一个四元组来标识一个TCP连接。即本地IP、本地端口、对端IP和对端端口。

服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端TCP连接四元组中只有对端IP和对端端口是可变的。

因此,最大TCP连接数 = 客户端IP数 × 客户端端口数。对于IPv4,客户端IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务端单机最大TCP连接数,约为2的48次方。

实际上,服务端的最大并发TCP连接数远不能达到理论上限。首先是文件描述符限制,上面说过,socket都是文件,所以首先通过ulimit配置文件描述符的数目;另一个限制是内存,每个TCP都要占用一定内存,操作系统是有限的。

所以,作为老板,在资源有限的情况下,要建立更多的项目,就需要降低每个项目消耗的资源数目。

方式一:将项目外包为其他公司(多进程方式)

这种情况,你就是一个代理,监听客户端来的请求。一旦建立连接,就会有一个已连接的socket,这时候你创建一个子进程,然后与整个socket的交互交给这个子进程来做。就像来了一个新项目,你不需要自己做,注册一个子公司,招点人,然后把项目转包给这家子公司做,以后对接就交给这家子公司,你就可以去接新的项目了。

如何创建子公司,并如何将项目移交给子公司呢?

在Linux下,创建子进程使用fork函数。这个函数在父进程的基础上完全拷贝一个子进程。在Linux内核中,会复制文件描述符的列表,也会复制内存空间。

进程复制过程如下图:

因为复制了文件描述符列表,而文件描述符都是指向整个内核统一的打开文件列表的,因而父进程刚才因为accept创建的已连接Socket也是一个文件描述符,同样也会被子进程获得。

接下来,子进程就可以通过这个已连接Socket和客户端进行互通了,当通信完毕之后,就可以退出进程,父进程如何直到子进程是否干完了项目,要退出呢?fork返回的时候会进行区分,如果是正整数就是父进程,这个正整数就是子进程的ID。

方式二:将项目转包给独立的项目组(多线程方式)

如果每次接一个项目就申请一个新公司,干完了再注销掉这个公司,实在是太麻烦了。毕竟一个新公司需要有新公司的资产,新的办公家具,每次买了卖,不划算。

我们可以用线程。相比于进程来说,线程更轻量。如果创建进程相当于成立新公司,创建线程相当于在同一个公司成立项目组。一个项目做完了,这个项目组就可以解散,组成另外的项目组,办公家具可以共用。

在Linux下,通过pthread_create创建一个线程,也是调用do_fork。不同的是很多资源,例如文件描述符表、进程空间,还是共享的,只是多一个引用而已。

新的线程也可以通过已连接Socket处理请求,从而达到并发处理的目的。

上面基于线程或进程模型的,还是有问题的。新到一个TCP连接,就需要分配一个进程或者线程。一台机器无法创建很多进程或者线程。有个C10K问题,是指一台机器要维护1万个连接,需要创建1万个进程或线程,那么操作系统是无法承受的。

方式三:一个项目组支撑多个项目(IO多路复用,一个线程维护多个socket)

一个项目组可以看多个项目。每个项目组都有个项目进度墙,将自己组看的项目列在哪里,然后通过项目墙看项目的进度,一旦某个项目有了进展,就派人盯一下。

socket是文件描述符,某个线程盯的所有socket,都放在一个文件描述符集合fd_set中,就是项目进度墙,然后调用select函数监听文件描述符集合是否有变化。一旦有变化,就会一次查看每个文件描述符。那些发生变化的文件描述符在fd_set对应的位设置为1,标识socket可读或可写,从而调用读写操作,然后再调用select,盯着下一轮的变化。

方式四:一个项目组支撑多个项目(IO多路复用,从“派人盯着”到“有事通知”)

select函数每次文件描述符集合中有socket发生变化时,都要通过轮询的方式,将全部项目查看一遍的方式来查看进度,这会影响一个项目组能够支撑的最大的项目数量。使用select,同时监听的描述符的最大数量由FD_SETSIZE限制。

如果改成时间通知的方式,就会好很多,不用通过轮询挨个盯着这些项目,而是当项目进度发生变化时,主动通知项目组,然后项目组根据进展情况做响应操作。完成这件事的函数叫做epoll,它在内核中的实现是通过注册callback函数的方式,当某个文件描述符发生变化,会主动通知。

这种方式使得监听的socket个数增加,效率也不会大幅度降低。上限是系统定义的进程打开的最大文件描述符个数。epoll被称为解决C10K问题的利器。

小结

  • 记住TCP和UDP的socket编程中,客户端和服务端需要调用的函数;
  • 写一个能够支撑大量连接的高并发的服务器不容易,需要多进程、多线程,而epoll机制能解决C10K问题。

思考题:

1. epoll是Linux上的函数,Windows上对应机制的函数是什么?如果实现一个跨平台的程序该怎么办?

2. 你知道HTTP的工作机制吗?

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值