传输层:套接字Socket

定义

  • Socket 可以作插口或者插槽讲,可以想象为弄一根网线,一头插在客户端,一头插在服务端,然后进行通信,所以在通信之前,双方都要建立一个Socket
  • Socket编程进行的是端到端的通信,能设置的参数只有网络层和传输层参数。
  • 网络层,Socket函数需要指定IPv4或IPv6,分别对应设置为AF_INET和AF_INET6。还需要指定TCP还是UDP,TCP协议是基于数据流的,所以设置为SOCK_STREAM,而UDP是基于数据报的,因而设置为SOCK_DGRAM。

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

  • TCP的服务端要先监听一个端口,一般是先调用bind函数,给这个Socket赋予一个IP地址和端口(bind函数参数)。
  • 为什么要有端口:当一个网络包来的时候 内核要通过TCP头里面的这个端口,来找到这个应用程序。
  • 为什么要有IP地址:因为一台机器可能有多个网卡,也就会有多个IP地址,可以选择监听所有的网卡,也可以选择监听一个网卡,只有发给目标网卡的包才会给你。
  • 服务端有了IP和端口号之后,可以调用listen函数进行监听。在TCP状态图中有一个listen状态,调用这个函数之后,服务端就进入了这个状态,此时客户端就可以发起连接了。
  • 在内核中,每个Socket维护了两个队列,一个是已经建立连接的队列,这时候三次握手已经完毕,处于established状态,一个是还没有完全建立连接的队列,这个时候三次握手还没有完成,处于sync_rcvd的状态。
  • 接下来,服务端调用accept函数,拿出一个已经完成的连接进行处理,如果没有已经完成的连接,就等待。
  • 在服务端等待的时候,客户端可以通过connect函数发起连接。在参数中指明IP和端口,然后发起三次握手,内核会给客户端分配一个临时的端口,一旦握手成功,服务端的accept就会返回另一个Socket。
  • 监听的Socket和真正用来传数据的Socket是两个,一个叫作监听Socket,一个叫做已连接Socket
  • 连接建立成功后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西。
    在这里插入图片描述
  • TCP的Socket就是一个文件流,Socket在Linux中就是以文件形式存在的,除此之外,还有文件描述符,写入和读出,也是通过文件描述符。
  • 在内核中,Socket是一个文件,那对应就有文件描述符,每一个进程都有一个数据结构task_struct,里面有一个文件描述数组,来列出这个进程打开的所有文件的文件描述符(打开表,文件描述符是一个非负整数,是数组的下标)。数组的元素是一个指针,指向内核中所有打开的文件列表中的某个元素,既然是文件就有inode,只不过Socket的inode是在内存里,指向Socket在内核中的Socket结构。
  • Socket结构主要有两个队列,一个发送队列,一个接收队列,在队列里保存的是一个缓存sk_buff,缓存里能看到完整的包结构。
    在这里插入图片描述

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

  • UDP没有三次握手,所以没有listenconnect,但是UDP的交互仍然需要IP和端口,所以需要bind
  • UDP没有维护连接状态,所以只用一个Socket和多个客户端通信,每次通信时调用sendorecvfrom,传入IP地址和端口,表明要和谁通信。
    在这里插入图片描述

服务器如何接入更多项目

  • {本机IP本机端口对端IP对端端口}四元组唯一标识一条TCP连接,通常本机IP和本机端口固定,所以理论上讲:
    最大TCP连接数= 客户端IP数(ipv4 2^32) * 客户端端口数(2^16),约2^48
  • 但是实际上远没有这么大,首先主要是文件描述符限制,ulimit参数限制了这点;而且每个TCP连接都得占内存(socket文件的inode就是在内存里),即服务器资源是有限的。有限的情况下,想接更多项目,就需要降低每个项目消耗。

方法一,多进程方式

  • 相当于代理,监听来的请求,一旦建立了连接,就会有一个已连接Socket,这时候可以创建一个子进程,然后将已连接Socket的交互任务交给子进程。
  • 在Linux下,创建子进程使用fork函数(这是在父进程基础上完全拷贝)。在Linux内核中,会复制文件描述符列表,也会复制内存空间,还会复制一条记录当前执行到了哪一行程序的进程。显然,复制时调用fork,复制完毕后,父进程和子进程都会记录当前刚刚执行完fork。这两个进程刚复制完,几乎一模一样,只是根据fork的返回值来区分,子进程fork返回0,父进程返回其他整数(这个整数是子进程的id)
  • 复制了文件描述符列表,而文件描述符指向的是内核统一打开的文件列表,父进程accept创建的已连接Socket也是一个文件描述符,所以同样会被子进程获得。
  • 子进程通过已连接Socket和客户端互通,通信完毕后退出进程,父进程通过fork返回值查看子进程是否完成任务,是否需要退出。
  • 多进程就像把项目外包给别的公司
    在这里插入图片描述

方法二,多线程方式

  • 进程比较重量级,申请和销毁成本较高(复制内存空间就是个问题)。我们还可以使用线程。
  • 在Linux下,通过pthread_create创建一个线程,也是调用do_fork。不同的是,虽然线程在task列表会创建新的一项,但是很多资源,例如文件描述符列表,进程空间还是共享的,只是多了一个引用。
  • 由于文件描述符列表共享,所以新的线程也可以通过文件描述符找到已连接Sokcet,进行处理(FDT File Description Table
  • 多线程就像把项目交给一个项目组,项目完成就解散
    在这里插入图片描述
    上述两种方法的问题在于:如果每有一个连接就分配一个进程和线程,一台机器的资源总归是有限的,无法创建很多进程或线程,C10K问题,一台机器维护1万个连接,就需要1万个进程或者线程,操作系统是承受不起的。

方法三:IO多路复用,一个线程盯着多个Socket

  • Socket是文件描述符,所以某个线程负责的所有Socket,都放在一个文件描述符集合fd_set中,然后调用select函数监听文件描述符集合是否有变化,一旦有变化,就会遍历文件描述符集合,发生变化的文件描述符在fd_set对应的位都设为1,表示Socket可读或者可写,从而可以进行读写操作。然后再调用select,进行下一轮监听。
  • 问题在于只要有一个Socket发生变化,就需要便利所有,影响了能够支持的最大Socket数量。使用select,能够同时盯着的数量由FD_SETSIZE限制
  • 相当于一个项目组负责多个项目,项目进度有变化就更新

方法四:IO多路复用,有事通知

  • epoll函数可以做到,它在内核中的实现不是通过轮询,而是注册callback,当某个文件描述符发生变化,就会调用callback主动通知。
    在这里插入图片描述
  • 假设进程打开了Socket m n x 等多个文件描述符,需要通过epoll监听这些socket是否都有事件发生,其中epoll_create创建一个epoll对象,也是一个文件,也对应一个文件描述符(epoll fd),也对应着打开文件列表中的一项。在这项里有一个红黑树,红黑树里保存这个epoll监听的所有Socket。
  • 当epoll_ctl添加一个Socket时,其实是加入红黑树,同事红黑树里的节点指向一个结构(epoll_entry),将这个结构挂在被监听的Socket事件列表中,当一个Socket来了一个事件是,可以从这个列表得到epoll对象,并调用callback通知。
  • 这种方式使得监听的Socket数据增加时,效率不会大幅降低,监听数目上限是进程打开的最大文件描述符个数。
  • epoll被称为解决C10K问题的利器
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值