socket所在网络架构和内部逻辑

socket所在网络架构和内部逻辑

基础部分

网络七层由下往上分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。 其中物理层、数据链路层和网络层通常被称作媒体层,是网络工程师所研究的对象; 传输层、会话层、表示层和应用层则被称作主机层,是用户所面向和关心的内容。

=网路架构

应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP等。应用层是不用去关心数据是如何传输的,就类似于,我们寄快递的时候,只需要把包裹交给快递员,由他负责运输快递,我们不需要关心快递是如何被运输的,应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。

传输层会有两个传输协议,分别是 TCP 和 UDP。TCP 的全称叫传输控制协议(Transmission Control Protocol),大部分应用使用的正是 TCP 传输层协议,比如 HTTP 应用层协议。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方;UDP 相对来说就很简单,简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。当然,UDP 也可以实现可靠传输,把 TCP 的特性在应用层上实现就可以,不过要实现一个商用的可靠 UDP 传输协议,也不是一件简单的事情。

网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文,如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文;一般用 IP 地址给设备进行编号,对于 IPv4 协议, IP 地址共 32 位,分成了四段(比如,192.168.100.1),每段是 8 位。只有一个单纯的 IP 地址虽然做到了区分设备,但是寻址起来就特别麻烦,全世界那么多台设备,难道一个一个去匹配?这显然不科学。

因此,需要将 IP 地址分成两种意义:

  • 一个是网络号,负责标识该 IP 地址是属于哪个「子网」的;
  • 一个是主机号,负责标识同一「子网」下的不同主机;

Socket

而socket是在TCP/UDP这一层的,本质上是对对 TCP/IP 协议的封装,Socket 本身并不是协议,而是一个调用接口(API),通过 Socket,我们才能使用 TCP/IP 协议。Socket 又称之为 “套接字”,是系统提供的用于网络通信的方法。它的实质并不是一种协议,没有规定计算机应当怎么样传递消息,只是给程序员提供了一个发送消息的接口,程序员使用这个接口提供的方法,发送与接收消息。

Socket 描述了一个 IP、端口对。它简化了程序员的操作,知道对方的 IP 以及 PORT 就可以给对方发送消息,再由服务器端来处理发送的这些消息。所以,Socket 一定包含了通信的双方,即客户端(Client)与服务端(server)。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机地址的 IP 地址,本地进程的协议端口,远地主机的 IP 地址,远地进程的协议端口。

应用层通过传输层进行数据通信时,TCP 会遇到同时为多个应用程序进程提供并发服务的问题。多个 TCP 连接或多个应用程序进程可能需要通过同一个 TCP 协议端口传输数据。为了区别不同的应用程序和连接,许多计算机操作系统为应用程序与 TCP/IP 协议交互提供了 Socket 接口。应用层可以和传输层通过 Socket 接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

建立 Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket,另一个运行于服务器端,称为 ServerSocket。

套接字之间的连接过程分为三个步骤:

(1)服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求

(2)客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求

(3)连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求

socket通信流程

服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

本地的进程间通信

(IPC)有很多种方式,但可以总结为下面 4 类:

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名的和具名的)
  • 远程过程调用(Solaris 门和 Sun RPC)

我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程 PID 来唯一标识一个进程,但是在网络中这是行不通的。其实 TCP/IP 协议族已经帮我们解决了这个问题,网络层的 “ip 地址” 可以唯一标识网络中的主机,而传输层的 “协议 + 端口” 可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip 地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

就目前而言,几乎所有的应用程序都是采用 socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说 “一切皆 socket”。

上面我们已经知道网络中的进程是通过 socket 来通信的,那什么是 socket 呢?socket 起源于 Unix,而 Unix/Linux 基本哲学之一就是 “一切皆文件”,都可以用 “打开 open –> 读写 write/read –> 关闭 close” 模式来操作。Socket 就是该模式的一个实现,socket 即是一种特殊的文件,socket 函数就是对其进行的操作(读 / 写 IO、打开、关闭),

3.1、SOCKET() 函数

int socket(int domain, int type, int protocol);

socket 函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字**,而 socket() 用于创建一个 socket 描述符(socket descriptor),它唯一标识一个 socket。**这个 socket 描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

它的参数

  • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称 AF_UNIX,Unix 域 socket)、AF_ROUTE 等等。协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 **ipv4 地址(32 位的)与端口号(16 位的)的组合、**AF_UNIX 决定了要用一个绝对路径名作为地址。

  • type:指定 socket 类型。常用的 socket 类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET 等等(socket 的类型有哪些?)。

sock_stream

​ 是有保障的 (即能保证数据正确传送到对方) 面向连接的 SOCKET,多用于资料 (如文件) 传送。

sock_dgram

​ 是无保障的面向消息的 socket , 主要用于在网络上发广播信息。

SOCK_STREAM 是基于 TCP 的,数据传输比较有保障。SOCK_DGRAM 是基于 UDP 的,专门用于局域网,基于广播 SOCK_STREAM 是数据流, 一般是 tcp/ip 协议的编程,

(补充知识点)(字节流是无边界的)

​ SOCK_DGRAM 分是数据报, 是 udp 协议网络编程

​ (数据报是记录型的数据报,所谓的记录型数据报就是接收进程可以识别接收到的数据报的记录边界)

  • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC 等,它们分别对应 TCP **传输协议、**UDP 传输协议、STCP 传输协议、TIPC 传输协议

注意:并不是上面的 type 和 protocol 可以随意组合的,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。当 protocol 为 0 时,会自动选择 type 类型对应的默认协议

3.2、BIND() 函数

正如上面所说 bind() 函数把一个地址族中的特定地址赋给 socket。例如对应 AF_INET、AF_INET6 就是把一个 ipv4 或 ipv6 地址和端口号组合赋给 socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数的三个参数分别为:

  • sockfd:即 socket 描述字,它是通过 socket() 函数创建了,唯一标识一个 socket。bind() 函数就是将给这个描述字绑定一个名字。

  • addr:一个 const struct sockaddr * 指针,指向要绑定给 sockfd 的协议地址。这个地址结构根据地址创建 socket 时的地址协议族的不同而不同,如 ipv4 对应的是:

如ipv4对应的是:
    struct sockaddr_in {
        sa_family_t    sin_family; 
        in_port_t      sin_port;   
        struct in_addr sin_addr;   
    };
    struct in_addr {
        uint32_t       s_addr;     
    };
ipv6对应的是:
    struct sockaddr_in6 { 
        sa_family_t     sin6_family;    
        in_port_t       sin6_port;      
        uint32_t        sin6_flowinfo;  
        struct in6_addr sin6_addr;      
        uint32_t        sin6_scope_id;  
    };
     
    struct in6_addr { 
        unsigned char   s6_addr[16];    
    };
Unix域对应的是:
#define UNIX_PATH_MAX    108
 
struct sockaddr_un { 
    sa_family_t sun_family;                
    char        sun_path[UNIX_PATH_MAX];   
};
  • addrlen:对应的是地址的长度。

通常服务器在启动的时候都会绑定一个众所周知的地址(如 ip 地址 + 端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的 ip 地址组合。这就是为什么通常服务器端在 listen 之前会调用 bind(),而客户端就不会调用,而是在 connect() 时由系统随机生成一个。

3.3、LISTEN()、CONNECT() 函数

如果作为一个服务器,在调用 socket()、bind() 之后就会调用 listen() 来监听这个 socket,如果客户端这时调用 connect() 发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen 函数的第一个参数即为要监听的 socket 描述字,第二个参数为相应 socket 可以排队的最大连接个数。socket() 函数创建的 socket 默认是一个主动类型的,listen 函数将 socket 变为被动类型的,等待客户的连接请求。

connect 函数的第一个参数即为客户端的 socket 描述字,第二参数为服务器的 socket 地址,第三个参数为 socket 地址的长度。客户端通过调用 connect 函数来建立与 TCP 服务器的连接。

3.4、ACCEPT() 函数

TCP 服务器端依次调用 socket()、bind()、listen() 之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket()、connect() 之后就想 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept() 函数取接收请求,这样连接就建立好了。之后就可以开始网络 I/O 操作了,即类同于普通文件的读写 I/O 操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

注意:accept 的第一个参数为服务器的 socket 描述字,是服务器开始调用 socket() 函数生成的,称为监听 socket 描述字;而 accept 函数返回的是已连接的 socket 描述字。一个服务器通常通常仅仅只创建一个监听 socket 描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接 socket 描述字,当服务器完成了对某个客户的服务,相应的已连接 socket 描述字就被关闭。

3.5、READ()、WRITE() 等函数

服务器与客户已经建立好连接后,可以调用网络 I/O 进行读写操作了,即实现了网咯中不同进程之间的通信!网络 I/O 操作有下面几组:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

C#中 recv()/send()
send
recv

我推荐使用 recvmsg()/sendmsg() 函数,这两个函数是最通用的 I/O 函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

sendmsg 和 recvmsg 这两个接口是高级套接口,这两个接口支持一般数据的发送和接收,还支持多缓冲区的报文发送和接收 (readv 和 sendv 支持多缓冲区发送和接收),还可以在报文中带辅助数据。这些功能是常用的 send、recv 等接口无法完成的。

1.  ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
2.  ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);

我们知道 tcp 建立连接要进行 “三次握手”,即交换三个分组。大致流程如下:

  • 客户端向服务器发送一个 SYN J
  • 服务器向客户端响应一个 SYN K,并对 SYN J 进行确认 ACK J+1
  • 客户端再想服务器发一个确认 ACK K+1

只有就完了三次握手,但是这个三次握手发生在 socket 的那几个函数中呢?请看下图:

三次握手

从图中可以看出,当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态;服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求向客户端发送 SYN K ,ACK J+1,这时 accept 进入阻塞状态;客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认;服务器收到 ACK K+1 时,accept 返回,至此三次握手完毕,连接建立。

上面介绍了 socket 中 TCP 的三次握手建立过程,及其涉及的 socket 函数。现在我们介绍 socket 中的四次握手释放连接的过程,请看下图:

四次挥手

图示过程如下:

  • 某个应用进程首先调用 close 主动关闭连接,这时 TCP 发送一个 FIN M;

  • 另一端接收到 FIN M 之后,执行被动关闭,对这个 FIN 进行确认。它的接收也作为文件结束符传递给应用进程,因为 FIN 的接收意味着应用进程在相应的连接上再也接收不到额外数据;

  • 一段时间之后,接收到文件结束符的应用进程调用 close 关闭它的 socket。这导致它的 TCP 也发送一个 FIN N;

  • 接收到这个 FIN 的源发送端 TCP 对它进行确认。

    这样每个方向上都有一个 FIN 和 ACK。

参考文章:

https://blog.csdn.net/weixin_41563161/article/details/103995604

https://blog.csdn.net/weixin_41563161/article/details/104148268

https://www.cnblogs.com/wangcq/p/3520400.html

https://www.jianshu.com/p/bf2e168d91f7

  • 21
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值