【webserver 前置知识 02】Linux网络编程入门其一

发布时间 2023-03-31 20:16:05作者: dayceng

网络结构模式

C/S结构

服务器 - 客户机,即 Client - Server(C/S)结构。C/S 结构通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。客户机是因特网上访问别人信息的机器,服务器则是提供信息供人访问的计算机。

在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。服务器部分是多个用户共享的信息与功能,执行后台服务,如控制共享数据库的操作等;客户机部分为用户所专有,负责执行前台功能

优点

  1. 能充分发挥客户端 PC 的处理能力,很多工作可以在客户端处理后再提交给服务器,所以 C/S 结构客户端响应速度快
  2. 操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求;
  3. C/S 结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程
  4. 安全性较高,C/S 一般面向相对固定的用户群,程序更加注重流程,它可以对权限进行多层次校验,提供了更安全的存取模式,对信息安全的控制能力很强,一般高度机密的信息系统采用 C/S 结构适宜。

缺点

  1. 客户端需要安装专用的客户端软件。首先涉及到安装的工作量,其次任何一台电脑出问题,如病毒、硬件损坏,都需要进行安装或维护。系统软件升级时,每一台客户机需要重新安装,其维护和升级成本非常高
  2. 对客户端的操作系统一般也会有限制,不能够跨平台

B/S结构

B/S 结构(Browser/Server,浏览器/服务器模式)中,WEB浏览器是客户端最主要的应用软件。这种模式统一了客户端,将系统功能实现的核心部分集中到服务器上,简化了系统的开发、维护和使用。客户机上只要安装一个浏览器,如 Firefox,服务器安装SQL Server、Oracle、MySQL 等数据库。浏览器通过 Web Server 同数据库进行数据交互。

优点

B/S 架构最大的优点是总体拥有成本低、维护方便分布性强、开发简单,可以不用安装任何专门的软件就能实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能使用。

缺点

  1. 通信开销大、系统和数据的安全性较难保障;
  2. 个性特点明显降低,无法实现具有个性化的功能要求;
  3. 协议一般是固定的:http/https(协议固定);
  4. 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低

mac地址、ip地址、端口

网卡与mac地址

先介绍一下网卡

网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为网络适配器或网络接口卡NIC。其拥有 MAC 地址,属于 OSI 模型的第2层,它使得用户可以通过电缆或无线相互连接。每一个网卡都有一个被称为 MAC 地址的独一无二的 48 位串行号。网卡的主要功能:1.数据的封装与解封装、2.链路管理、3.数据编码与译码

然后是mac地址

MAC地址(Media Access Control Address),直译为媒体存取控制位址,也称为局域网地址以太网地址物理地址硬件地址

MAC地址用来确认网络设备位置的位址,由网络设备制造商生产时烧录在网卡中。

在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 位址。

MAC 地址用于在网络中唯一标识一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的 MAC 地址。

IP地址

简介

IP协议是为计算机网络相互连接进行通信而设计的协议。在因特网中,它是能使连接到网上的所有计算机网络实现相互通信的一套规则,规定了计算机在因特网上进行通信时应当遵守的规则。任何厂家生产的计算机系统,只要遵守 IP 协议就可以与因特网互连互通。

各个厂家生产的网络系统和设备,如以太网、分组交换网等,它们相互之间不能互通,不能互通的主要原因是因为它们所传送数据的基本单元(技术上称之为“帧”)的格式不同

IP协议(实际上是一套由软件程序组成的协议软件)可以把各种不同“帧”统一转换成“IP 数据报”格式

IP地址(Internet Protocol Address)是指互联网协议地址,又译为网际协议地址。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址(mac)的差异

IP 地址是一个32位的二进制数,通常被分割为4个 “8位二进制数”(也就是4个字节)。

IP 地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是 0~255 之间的十进制整数。
例:点分十进IP地址(100.4.5.6),实际上是 32 位二进制数(01100100.00000100.00000101.00000110)。

子网掩码

子网掩码(subnet mask)又叫网络掩码、地址掩码、子网络遮罩,它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合 IP 地址一起使用。子网掩码只有一个作用,就是将某个 IP 地址划分成网络地址(用于标识网络)和主机地址(用于标识该网络中的主机)两部分。

例如:192.168.1.1是一个IPv4地址,其中192.168是网络地址,1.1是主机地址。

作用:

  • 在 IPv4 地址资源紧缺的背景下为了解决 IP 地址分配而产生的虚拟 IP 技术(通过子网掩码将 A、B、C 三类地址划分为若干子网,从而显著提高了 IP 地址的分配效率)
  • 方便网络管理

端口

“端口”,即port,可以认为是设备与外界通讯交流的出口。

端口可分为虚拟端口物理端口

虚拟端口指计算机内部交换机路由器内的端口(不可见),特指TCP/IP协议中的端口,是逻辑意义上的端口。(例如80端口、21端口、23端口等)

物理端口又称为接口,是可见端口,例如 RJ45网口

端口类型

1、周知端口

顾名思义,这些端口在功能上是众所周知的,大家都默认将其作为一类特有功能的专有端口

例如:80 端口分配给 WWW 服务,21 端口分配给 FTP 服务,23 端口分配给 Telnet服务等等

2、注册端口

端口号从 1024 到 49151,它们松散地绑定于一些服务,分配给用户进程或应用程序,这些进程主要是用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资源占用的时候,可以用用户端动态选用为源端口。

3、动态端口 / 私有端口

动态端口的范围是从 49152 到 65535。之所以称为动态端口,是因为它一般不固定分配某种服务,而是
动态分配。

网络模型

OSI 七层参考模型

============================================
7.应用层
针对特定应用的协议【例如电子邮件、文件传输等】
============================================
6.表示层
设备固有数据格式和网络标准数据格式的转换
【接收文字流、图像、声音等】
实际应用中用于保证异构机型之间的数据一致性

例如:从网易邮箱发往QQ邮箱的数据,在表示层中转换为各自软件的专属数据格式,而在表示层之下则使用统一的网络数据格式进行传输
============================================
5.会话层
通信管理。负责建立和断开通信连接,是数据流动的逻辑通路。管理传输层以下的分层【何时建立\断开连接,连多久】

还是用发邮件举例,该层决定邮件应该怎么发,在其收到的数据前端附加首部或标签信息,这些部分记录着数据传送顺序的信息
============================================
===========以下层负责具体数据的传输=============
4.传输层
管理两个节点(互联的网络终端设备)之间的数据传输,负责传输的可靠性,确保数据传送至目的地
【判断数据是否有丢失】
若传输过程中出现异常导致目的机器未收到数据,目的机器会再次询问发送方,发送方会将剩余部分重新发送并再次确认是否送达。
=================↑=协作=↓===================
3.网络层
地址管理和路由选择【选择经过哪个路由传递到目标地址】

实际将数据从A送到B
============================================
2.数据链路层
互联设备之间传送和识别数据帧【数据帧与比特流(如0101)之间的转换,数->比->数】
    
通过传输介质互联的设备,进行数据处理
============================================
1.物理层
以0、1代表高低电平和灯光的亮灭,界定连接器和网线的规格【比特流与电子信号(模拟信号)之间的切换,比->电->比】

直连设备之间采用地址实现传输,这种地址称为MAC地址,目的是为了识别连接到同一个传输介质上的设备。    

TCP/IP 四层模型

简介

现在 Internet(因特网)使用的主流协议族是 TCP/IP 协议族,它是一个分层、多协议的通信体系。

TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层网络层传输层应用层

每一层完成不同的功能,且通过若干协议来实现,上层协议使用下层协议提供的服务。

在 TCP/IP 协议中,OSI 模型被简化为了四个层次。

(1)应用层、表示层、会话层三个层次提供的服务相差不是很大,所以在 TCP/IP 协议中,它们被合并为应用层一个层次。

(2)由于传输层和网络层在网络协议中的地位十分重要,所以在 TCP/IP 协议中它们被作为独立的两个层次。

(3)因为数据链路层和物理层的内容相差不多,所以在 TCP/IP 协议中它们被归并在网络接口层一个层次里。只有四层体系结构的 TCP/IP 协议,与有七层体系结构的 OSI 相比要简单了不少,也正是这样,TCP/IP 协议在实际的应用中效率更高,成本更低

四层介绍

  1. 应用层:应用层是 TCP/IP 协议的第一层,是直接为应用进程提供服务
    (1)对不同种类的应用程序它们会根据自己的需要来使用应用层的不同协议,邮件传输应用使用了 SMTP 协议、万维网应用使用了 HTTP 协议、远程登录服务应用使用了有 TELNET 协议。
    (2)应用层还能加密、解密、格式化数据。
    (3)应用层可以建立或解除与其他节点的联系,这样可以充分节省网络资源。
  2. 传输层:作为 TCP/IP 协议的第二层,运输层在整个 TCP/IP 协议中起到了中流砥柱的作用。且在运输层中,TCP 和 UDP 也同样起到了中流砥柱的作用。
  3. 网络层:网络层在 TCP/IP 协议中的位于第三层。在 TCP/IP 协议中网络层可以进行网络连接的建立和终止以及 IP 地址的寻找等功能。
  4. 网络接口层:在 TCP/IP 协议中,网络接口层位于第四层。由于网络接口层兼并了物理层和数据链路层所以,网络接口层既是传输数据的物理媒介,也可以为网络层提供一条准确无误的线路。

协议

网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。

网络协议的三要素是:语法、语义、时序

协议(protocol)往往分成几个层次进行定义,分层定义是为了使某一层协议的改变不影响其他层次的协议。

常见协议

应用层常见的协议有:FTP协议(File Transfer Protocol 文件传输协议)、HTTP协议(Hyper Text Transfer Protocol 超文本传输协议)、NFS(Network File System 网络文件系统)

传输层常见协议有:TCP协议(Transmission Control Protocol 传输控制协议)、UDP协议(User Datagram Protocol 用户数据报协议)

网络层常见协议有:IP 协议(Internet Protocol 因特网互联协议)、ICMP 协议(Internet Control Message Protocol 因特网控制报文协议)、IGMP 协议(Internet Group Management Protocol 因特网组管理协议)

网络接口层常见协议有:ARP协议(Address Resolution Protocol 地址解析协议)、RARP协议(Reverse Address Resolution Protocol 反向地址解析协议)

UDP协议

  1. 源端口号:发送方端口号
  2. 目的端口号:接收方端口号
  3. 长度:UDP用户数据报的长度,最小值是8(仅有首部)
  4. 校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃

TCP协议

  1. 源端口号:发送方端口号
  2. 目的端口号:接收方端口号
  3. 序列号:本报文段的数据的第一个字节的序号
  4. 确认序号:期望收到对方下一个报文段的第一个数据字节的序号
  5. 首部长度(数据偏移):TCP报文段的数据起始处距离 TCP 报文段的起始处有多远,即首部长
    度。单位:32位,即以 4 字节为计算单位
  6. 保留:占6位,保留为今后使用,目前应置为0
  7. 紧急URG:此位置 1 ,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
  8. 确认ACK:仅当 ACK=1 时确认号字段才有效,TCP 规定,在连接建立后所有传达的报文段都必须把ACK置1
  9. 推送PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP 就可以使用推送(push)操作,这时,发送方 TCP 把 PSH 置 1,并立即创建一个报文段发送出去,接收方收到 PSH = 1 的报文段,就尽快地(即“推送”向前)交付给接收应用进程,而不再等到整个缓存都填满后再向上交付
  10. 复位RST:用于复位相应的 TCP 连接
  11. 同步SYN:仅在三次握手建立 TCP 连接时有效。当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文段,对方若同意建立连接,则应在相应的报文段中使用 SYN = 1 和 ACK = 1。因此,SYN 置1就表示这是一个连接请求或连接接受报文
  12. 终止FIN:用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接
  13. 窗口:指发送本报文段的一方的接收窗口(而不是自己的发送窗口)
  14. 校验和:校验和字段检验的范围包括首部和数据两部分,在计算校验和时需要加上 12 字节的伪头部
  15. 紧急指针:仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据
  16. 选项:长度可变,最长可达 40 字节,当没有使用选项时,TCP 首部长度是 20 字节

IP协议

  1. 版本:IP协议的版本。通信双方使用过的 IP 协议的版本必须一致,目前最广泛使用的 IP 协议版本号为 4(即IPv4)
  2. 首部长度:单位是 32 位(4 字节)
  3. 服务类型:一般不适用,取值为 0
  4. 总长度:指首部加上数据的总长度,单位为字节
  5. 标识(identification):IP 软件在存储器中维持一个计数器,每产生一个数据报,计数器就加 1,并将此值赋给标识字段
  6. 标志(flag):目前只有两位有意义。标志字段中的最低位记为 MF。MF = 1 即表示后面“还有分片”的数据报。MF = 0 表示这已是若干数据报片中的最后一个。标志字段中间的一位记为 DF,意思是“不能分片”,只有当 DF = 0 时才允许分片
  7. 片偏移:指出较长的分组在分片后,某片在源分组中的相对位置,也就是说,相对于用户数据段的起点,该片从何处开始。片偏移以 8 字节为偏移单位。
  8. 生存时间TTL,表明是数据报在网络中的寿命,即为“跳数限制”,由发出数据报的源点设置这个字段。路由器在转发数据之前就把 TTL 值减一,当 TTL 值减为零时,就丢弃这个数据报。
  9. 协议:指出此数据报携带的数据时使用何种协议,以便使目的主机的 IP 层知道应将数据部分上交给哪个处理过程,常用的 ICMP(1),IGMP(2),TCP(6),UDP(17),IPv6(41)
  10. 首部校验和:只校验数据报的首部,不包括数据部分。
  11. 源地址:发送方 IP 地址
  12. 目的地址:接收方 IP 地址

ARP协议

  1. 硬件类型:1表示 MAC 地址
  2. 协议类型:0x800 表示 IP 地址
  3. 硬件地址长度:6
  4. 协议地址长度:4
  5. 操作:1表示 ARP 请求,2表示 ARP 应答,3表示 RARP 请求,4表示 RARP 应答

封装

上层协议是如何使用下层协议提供的服务的呢?其实这是通过封装(encapsulation)实现的。

应用程序数据在发送到物理网络上之前,将沿着协议栈从上往下依次传递。每层协议都将在上层数据的基础上加上自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程就称为封装。

以太网帧传到目标主机后还会一层一层的将各层添加的信息剥离(过程中会根据这些信息做出相应的下一步处理),最后得到应用要发送的数据

网络通信过程

socket介绍

所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象

一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。

socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。

什么意思呢?感觉还是不太理解?举个例子

有两台Linux主机A、B,其分别建立了socket进行通信,如下图所示

A、B通过端口(虚拟端口)达成建立链接,然后分别建立了一个socket用于传输数据

socket由一个文件操作符fd控制,通过对fd进行数据写入和读取,可以获取对方主机发送的数据

socket在其中负责两部分工作

一方面,socket要去通信协议中获取各个层的信息,并处理(解封)数据,得到可以直接读取的数据放入读缓冲区

另一方面,通过fd写入的待发送数据被存入写缓冲区,socket还是需要根据对应的协议将其进行封装,最后以以太网帧的形式发出(发出不归socket管)

字节序

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。

字节序分为 大端字节序(Big-Endian)和 小端字节序(Little-Endian)【面试】

大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地
址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地、址处。

什么意思呢?举个例子

假设主机A(小端字节序)有数据:0x01020304

那么该数据在内存中的存放顺序是:04030201

字节序转换函数

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。

解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小 端机转换,大端机不转换)

网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式

BSD Socket提供了封装好的转换接口,方便程序员使用。

包括从主机字节序到网络字节序的转换函数:htonshtonl

从网络字节序到主机字节序的转换函数:ntohsntohl

h   - host 主机,主机字节序
to  - 转换成什么
n   - network  网络字节序
s   - short  unsigned short
l   - long   unsigned int
#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort);     // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort);      // 主机字节序 - 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong);      // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong);       // 主机字节序 - 网络字节序

socket 地址

socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。客户端 -> 服务器(IP, Port)

通用 socket 地址

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
};
typedef unsigned short int sa_family_t;

IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)

下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:

#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
    af:地址族: AF_INET  AF_INET6
    src:需要转换的点分十进制的IP字符串
    dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    af:地址族: AF_INET  AF_INET6
    src: 要转换的ip的整数的地址
    dst: 转换成IP地址字符串保存的地方
    size:第三个参数的大小(数组的大小)
    返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

TCP通信流程

TCP 和 UDP对比

// TCP 和 UDP  -> 传输层的协议
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
    
                 UDP                            TCP
是否创建连接       无连接                        面向连接
是否可靠          不可靠                        可靠的
连接的对象个数     一对一、一对多、多对一、多对多     支持一对一
传输的方式        面向数据报                     面向字节流
首部开销         8个字节                        最少20个字节
适用场景         实时应用(视频会议,直播)         可靠性高的应用(文件传输)

TCP 服务器端流程

// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
    - 监听:监听有客户端的连接
    - 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
    - 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)
5. 通信
        - 接收数据
        - 发送数据
6. 通信结束,断开连接

TCP 客户端流程

// 客户端
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
    - 接收数据
    - 发送数据
4. 通信结束,断开连接

套接字函数

简介

#include <sys/types.h>      
#include <sys/socket.h>
#include <arpa/inet.h>  // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
    - 功能:创建一个套接字
    - 参数:
        - domain: 协议族
            AF_INET : ipv4
            AF_INET6 : ipv6
            AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
        - type: 通信过程中使用的协议类型
            SOCK_STREAM : 流式协议
            SOCK_DGRAM  : 报式协议
        - protocol : 具体的一个协议。一般写0
            - SOCK_STREAM : 流式协议默认使用 TCP
            - SOCK_DGRAM  : 报式协议默认使用 UDP
        - 返回值:
            - 成功:返回文件描述符,操作的就是内核缓冲区。
            - 失败:-1         
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
    - 功能:绑定,将fd 和本地的IP + 端口进行绑定
    - 参数:
            - sockfd : 通过socket函数得到的文件描述符
            - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
            - addrlen : 第二个参数结构体占的内存大小
    
int listen(int sockfd, int backlog);    // /proc/sys/net/core/somaxconn
    - 功能:监听这个socket上的连接
    - 参数:
        - sockfd : 通过socket()函数得到的文件描述符
        - backlog : 未连接的和已经连接的和的最大值, 5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 
    - 参数:
            - sockfd : 用于监听的文件描述符
            - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
            - addrlen : 指定第二个参数的对应的内存大小
    - 返回值:
            - 成功 :用于通信的文件描述符 
            - -1 : 失败
                
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 - 功能: 客户端连接服务器
    - 参数:
            - sockfd : 用于通信的文件描述符
            - addr : 客户端要连接的服务器的地址信息
            - addrlen : 第二个参数的内存大小
    - 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count);   // 写数据
ssize_t read(int fd, void *buf, size_t count);          // 读数据

TCP通信实现(服务器端)

完全是按照之前介绍的流程实现的,简单回顾一下:

  1. 创建一个用于监听的套接字(创建socket)
  2. 将这个监听文件描述符和本地的IP和端口绑定(绑定IP端口)
  3. 设置监听,监听的fd开始工作(监听)
  4. 接收客户端连接(阻塞)
  5. 通信(收发数据)
  6. 通信结束,断开连接
//TCP通信服务端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(){
    //1、创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }
    
    //2、绑定
    struct sockaddr_in saddr;//先定义一个socket地址结构体
    //初始化结构体内成员变量
    saddr.sin_family = AF_INET;
    //转换ip地址,把主机字节序转换为网络字节序
  
    saddr.sin_addr.s_addr = INADDR_ANY;//相当于0.0.0.0
    saddr.sin_port = htons(9999);//网络字节序
    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
    
    if(ret == -1){
        perror("bind");
        exit(-1);
    }
    
    //3、监听
    ret = listen(lfd, 8);//
    if(ret == -1){
        perror("listen");
        exit(-1);
    }
    
    //4、接收客户端连接
    //客户端地址信息
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
    if(cfd == -1){
        perror("accept");
        exit(-1);
    }
    
    //输出客户端信息
    //需要将网络字节序转换为主机字节序
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);
    
    //5、通信过程
    char recvBuf[1024] = {0};
    while(1){
        //获取客户端数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1){
            perror("read");
            exit(-1);
        }else if(num > 0){
            printf("recv client data: %s\n", recvBuf);
        }else if(num == 0){//表示客户端断开连接
            printf("client closed...");
            break;
        }
        //给客户端发送数据
        char* data = "hello motherfucker";
        write(cfd, data, strlen(data));
    }   
    //关闭文件描述符
    close(cfd);
    close(lfd);
    
    return 0;
}

TCP通信实现(客户端端)

1、创建socket

2、连接服务器(指定IP端口)

3、收发数据

4、通信结束,断开连接

//TCP通信客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>


int main(){
    //1、创建套接字
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1){
        perror("socker");
        exit(-1);
    }

    //2、连接服务器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = PF_INET;
    inet_pton(PF_INET, "192.168.164.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    if(ret == -1){
        perror("connect");
        exit(-1);
    }
    
    //3、通信
    char recvBuf[1024] = {0};
    char str[100];
    while (1){
        printf( "输入你要发送的 :");
        scanf("%s", str);
        // char* data = "hello, u motherfuck";
        //给客户端发送数据
        // write(fd, data, strlen(data));
        write(fd, str, strlen(str));
        // sleep(1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1){
            perror("read");
            exit(-1);
        }else if(len > 0){
            printf("recv server data: %s\n", recvBuf);
        }else if(len == 0){//表示客户端断开连接
            printf("server closed...");
            break;
        }
    }
     
    //关闭连接
    close(fd);
    return 0;
}

TCP 三次握手

简介

TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连
接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。

TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。

TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用四次挥手来关闭一个连接。

例子

三次握手的目的是保证双方相互之间建立了连接

三次握手发生在客户端连接的时候,当调用connect()时,底层会通过TCP协议进行三次握手

下面通过一个例子来走一遍三次握手的过程

ws3

第一次握手(客户端发送第一个SYN报文)

​ 客户端通过发送SYN确认自己发送是ok的,服务端接收到SYN可以确认自己的接收是ok的(同时确认客户端是能发送的)

​ 客户端随机初始化序列号 client_isn ,放进TCP⾸部序列号段,然后把 SYN 置1,把 SYN 报文发
送给服务端,表示发起连接,之后客户端处于 SYN-SENT 状态。

第二次握手(服务端发送第二个SYN报文+确认报文ACK)

​ 客户端通过服务端发来的ACK确认自己的接收是没问题的,同时确认服务端的发送是没问题的

​ 服务端收到客户端的 SYN 报文,把自己的序号 server_isn 放进TCP⾸部序列号段。

​ 确认应答号,填⼊ client_ins + 1 ,把 SYN+ACK 置1。

​ 把 SYN+ACK 报⽂发送给客户端,然后进⼊SYN-RCVD状态。

第三次握手(客户端发送确认报文Ack)

​ 客户端收到服务端报文后,还要向服务端回应最后⼀个应答报文

​ 首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次确认应答号字段填⼊ server_isn + 1 ,最后把报文发送给服务端,本次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。服务器收到客户端的应答报文后,也进入ESTABLISHED 状态。

到此,三次握手结束

讨论:为什么需要三次握手?

总的来说可以归结为以下原因:

  1. 三次握手可以避免重复初始化历史连接(主要原因)
  2. 三次握手可以同步双方的初始序列号
  3. 三次握手能够避免资源浪费

下面来具体讨论原因

避免重复历史连接的初始化

设想这样一个情况:客户端先给服务端发了一个SYN报文(旧报文),但由于网络拥堵,客户端触发了超时重传,此时客户端又会发出一个SYN报文(新报文)。当旧的SYN报文先到达服务端,服务端回⼀个ACK+SYN报文,客户端收到后可以根据自身的上下文,判断这是⼀个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示终止这⼀次连接。

在上述情况中,如果使用的是两次握手,那么在收到服务器的相应后,客户端会立刻发送数据,就不能判断该连接是否存在问题。三次握手则可以在准备发送第三次报文(Ack)期间来判断历史连接

同步双方的初始序列号

序列号是可靠传输的关键因素,因此使用TCP协议通信的双方,都必须维护一个序列号

序列号的作用

使用序列号:

  • 接收端可以去除重复数据
  • 接收端可以按照序列号顺序接收
  • 标识发送的数据包(哪些已经被收到)
同步过程

1、客户端发送第一个报文(携带客户端初始序列号的SYN报文)

2、服务器发送第二个报文(携带服务器初始序列号的ACK+SYN报文)

3、客户端发送第三个报文(携带服务器的ACK应答报文)

两次握手只保证了一方的初始序列号能被对⽅成功接收,没办法保证双方的初始序列号都能被
确认接收。

避免资源浪费(两次握手的问题)

1、在有消息滞留的情况下,两次握手会导致服务器重复接收无用的连接请求报文,造成资源重复分配,三次握手避免了这种情况。

2、只有两次握手时,如果客户端的SYN请求连接在网络中阻塞,客户端没有收到服务端的ACK报文,只能重新发送SYN。由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接。三次握手避免了这种情况。

TCP 四次挥手

简介

​ 四次挥手发生在断开连接的时候,在程序中当调用了 close() 会使用CP协议进行四次挥手客户端和服务器端都可以主动发起断开连接,谁先调用 close() 谁就是发起。

​ 因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开

例子

下面以客户端调用close()发起断开连接的过程为例,梳理一下四次挥手过程

ws4

断开连接的过程:

​ 1、假设客户端打算关闭连接,发送一个TCP首部 FIN 被置1的FIN报文给服务端

​ 2、服务端收到以后,向客户端发送 ACK 应答报文

​ 3、等待服务端处理完数据后,向客户端发送 FIN 报文。客户端接收到 FIN 报文后回一个 ACK 应答报文

​ 4、服务器收到 ACK 报文后,进入 close 状态,服务器完成连接关闭

​ 5、客户端在经过 2MSL 一段时间后,自动进入 close 状态,客户端也完成连接的关闭

讨论

1、为什么要四次挥手?

客户端发起的断开连接为例

当需要关闭连接时,客户端发送FIN报文,表示其不再发送数据,但此时客户端还可以接收数据

服务端收到客户端发的FIN报文后会先回复一个ACK应答报文,因为服务端可能还有数据需要处理和发送,等待这部分工作结束之后,服务端才会发送FIN报文给客户端

由上述过程可知:

​ 1、由于服务端需要处理未发送完成的数据,所以服务端的ACK和FIN 一般是分开发送的

​ 2、延迟确认: 即接收方收到包后,如果暂时没有内容回复给发送方,则延迟一段时间再确认,假如在这个时间范围内刚好有数据需要传输,则和确认包一起回复。这种也被称为数据捎带。延迟确认只是减轻网络负担,未必可以提升网络性能,有些情况下反而会影响性能。

​ 3、如果出现延迟确认,则第一次ACK应答报文可以省略(因为延迟确认后发送的报文中也带有ACK)

2、为什么TIME_WAIT等待时间是2MSL?

MSL是 Maximum Segment Lifetime的缩写,即报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

为什么要等待两倍MSL?

网络中可能存在发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文那么 2MSL 时间将重新计时。

3、为什么需要TIME_WAIT状态?

需要注意的是,主动发起关闭连接的一方才会有TIME_WAIT状态

需要该状态的主要原因是:

  • 防止具有相同 四元组 的 “旧”数据包被接收
  • 保证“被动”关闭连接的一方能被正确的关闭,具体来说就是要保证最后发送的ACK能让被动关闭方接收到从而帮助其正常关闭

有相同端口的 TCP 连接被复用后,被延迟的相同四元组的数据包抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的

如果最后的ACK丢失,客户端将直接进入 close ,服务端则一直在等待 ACK 状态。当客户端发起建立连接的SYN请求,服务端会发送RST报文回应,连接建立会关闭。

在TIME_WAIT状态中可能会有以下两种情况:

​ 1、服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。

​ 2、服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。

4、过多的TIME_WAIT状态有什么危害?

​ 占用内存与端口资源

TCP 滑动窗口

简介

滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的
拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,
谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种
技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包
(称窗口尺寸)。
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于
接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0
时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制(控制每次发送/接收多少数据)、拥塞控制的承载结构。

"窗口"可以理解为缓冲区的大小

滑动窗口的大小会随着发送数据和接收数据的变化而变化

通信双方都有发送和接收数据的缓冲区

发送方的缓冲区
白色格子:空闲的空间灰色格子:数据已经被发送出去了,但是还没有被接收
紫色格子:还没有发送出去的数据

接收方的缓冲区
白色格子:空闲的空间
紫色格子:已经接收到的数据

讨论

1、什么是“窗口”?

TCP每发送一个数据,都需要一次应答,然后继续发送。

这样为每个数据包都进行确认应答的缺点是:数据往返时间越长,网络吞吐量越低。

所以引入"窗口"的概念,即使往返时间较长,也不会降低网络通信效率

窗口大小可以指定,窗口的范围就是无需等待确认应答便可继续发送数据的最大值。

窗口实现就是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

这个模式叫做累计确认或者累计应答

2、“窗口”的大小由什么决定?

TCP头部有一个字段叫window,即窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,从而避免接收端来不及处理发送到的数据。

通常窗口的大小是由接收方的窗口大小来决定的

拥塞控制

拥塞控制通过拥塞窗口来防止过多的数据注入网络,使得网络中的路由器或者链路过载。

  • 拥塞窗口cwnd是发送方维护的一个状态变量,根据网络拥塞程度而变化。
  • 发送窗口的值是swnd = min(cwnd,rwnd),也就是拥塞窗口和接收窗口中的最小值
  • 网络中没有出现拥塞,cwnd增大,出现拥塞,cwnd减小。

其实只要发送方没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞

拥塞控制算法

1、慢启动

​ 慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加

2、拥塞避免算法

​ 每当收到一个 ACK 时,cwnd 增加1/cwnd(将慢启动算法的指数增长变成了线性增长

这会使网络慢慢地进入拥塞状态,直到出现丢包现象,这是对丢失的数据包进行重传即可(即触发了拥塞发生算法)

3、拥塞发生算法

​ 发⽣拥塞,进行数据重传(超时重传、快速重传)

4、快速恢复

快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。