Socket.IO 是一个库,可以在客户端和服务器之间实现低延迟双向基于事件的通信

Socket.IO 是一个面向实时 web 应用JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和一个面向Node.js的服务端库。两者有着几乎一样的API。像Node.js一样,它也是事件驱动的.

Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io可以回退到几种其它方法,例如Adobe Flash Sockets,JSONP拉取,或是传统的AJAX拉取),并且在同时提供完全相同的接口。尽管它可以被用作WebSocket的包装库,它还是提供了许多其它功能,比如广播至多个套接字,存储与不同客户有关的数据,和异步IO操作

可以使用npm(node 软件包)工具来安装。

Socket 为客户端和服务器提供了双向通信机制。

这意味着服务器可以 推送 消息给客户端。无论何时你发布一条消息,服务器都可以接收到消息并推送给其他连接到服务器的客户端。

Socket.IO 的核心理念就是允许发送、接收任意事件和任意数据。任意能被编码为 JSON 的对象都可以用于传输。二进制数据也是支持的。

这里的实现方案是,当用户输入消息时,服务器接收一个 chat message 事件。index.html 文件中的 script 部分现在应该内容如下:

接下来的目标就是让服务器将消息发送给其他用户。

要将事件发送给每个用户,Socket.IO 提供了 io.emit 方法:



socket(套接字?插槽)

Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。“Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,提供一套调用TCP/IP协议的API。

image-20220711204509818

所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。

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

socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

// 套接字通信分两部分:
- 服务器端:被动接受连接,一般不会主动发起连接
- 客户端:主动向服务器发起连接
socket是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别

websocket和http的关系

相同点

1、都是基于TCP的,都是可靠传输协议。

2、都是应用层协议。

不同点

image-20220711203604868

Http、Socket、WebSocket之间联系与区别

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

可以把WebSocket想象成HTTP(应用层),HTTP和Socket什么关系,WebSocket和Socket就是什么关系。

HTTP 协议有一个缺陷:通信只能由客户端发起,做不到服务器主动向客户端推送信息。

WebSocket 协议 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种


socket和http的区别:
Http协议:简单的对象访问协议,对应于应用层。Http协议是基于TCP链接的。
tcp协议:对应于传输层
ip协议:对应于网络层
TCP/IP是传输层协议,主要解决数据如何在网络中传输;而Http是应用层协议,主要解决如何包装数据。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。


Http连接:http连接就是所谓的短连接,及客户端向服务器发送一次请求,服务器端相应后连接即会断掉。

socket连接:socket连接及时所谓的长连接,理论上客户端和服务端一旦建立连接,则不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该链接已释放网络资源。所以当一个socket连接中没有数据的传输,那么为了位置连续的连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的。


1.HTTP的长连接一般就只能坚持一分钟而已,而且是浏览器决定的,你的页面很难控制这个行为。
Socket连接就可以维持很久,几天、数月都有可能,只要网络不断、程序不结束,而且是可以编程灵活控制的。
2.HTTP连接是建立在Socket连接之上。在实际的网络栈中,Socket连接的确是HTTP连接的一部分。但是从HTTP协议看,它的连接一般是指它本身的那部分。


TCP/IP协议栈主要分为四层:应用层、传输层、网络层、数据链路层,

每层都有相应的协议,如下图(TCP/IP四层模型)

应用层包括http(万维网)、ftp(文件传输)、smtp(电子邮件)、dns、telnet

img

image-20220716100956055

之前不太懂为什么有OSI七层模型、四层模型,其实他们的区分标准在于OSI七层模型是理论上的分层方式,而四层模型是实践过程中的分层模型。

OSI七层模型ISO(国际标准化组织)提出的一套理论性的网络标准化协议,可以把它看成一本教科书,它在指定之前是没有经过实践的,而为什么我们实践的过程中又没有遵循OSI的标准来分层呢,是因为我们在实践的过程中发现有些功能不必要分得那么细,而TCP四层模型就是我们实践过程中发现比较合理的分层,虽然我们实际过程中都没有按OSI分为七层,但是OSI对我们实践过程分层有着指导性的意义。

五层协议将四层协议中的网络接口层分为数据链路层和物理层

img

TPC/IP协议栈,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。

IP想像两个站点TCP和UDP是高速公路,HTTP 、FTP 就是货车,而 Socket 就是两个站点的检票口。货车(http)在一端站点(ip)先通过检票口(socket),检票后行驶在高速公路(tcp)上,到达另一站点(ip)卸载货物(http)。


https://www.jianshu.com/p/4e80b931cdea

Socket.io

Socket.io提供了基于事件的实时双向通讯

Browser和WebServer间的实时数据传输是一个很重要的需求,但最早只能通过AJAX轮询方式实现。在WebSocket标准没有推出之前,AJAX轮询是一种可行的方案。

AJAX轮询原理是设置定时器,定时通过AJAX同步服务端数据。这种方式存在延时且对服务端造成很大负载。直至2011年,IETF才标准化WebSocket - 一种基于TCP套接字进行收发数据的协议。

WebSocket 协议

WebSocket是HTML5新增的一种通信协议,其特点是服务端可以主动向客户端推送信息,客户端也可以主动向服务端发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后浏览器和服务端之间就形成了一条快速通道,两者之间就直接可以数据相互传送,带来的好处是

  1. 相互沟通的Header很小,大概只有2Bytes。
  2. 服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。

为了建立一个WebSocket连接,浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息Upgrade: WebSocket表明这是一个申请协议升级的HTTP请求。服务端解析这些头信息,然后产生应答信息返回给客户端,客户端和服务端的WebSocket连接就建立起来了。双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续直到客户端或者服务端的某一方主动关闭连接。

为什么要使用WebSocket呢?

Browser已经支持HTTP协议,为什么还要开发一种新的WebSocket协议呢?

我们知道HTTP协议是一种单向的网络协议,在建立连接后,仅允许Browser/UserAgent向WebServer发出请求资源后,WebServer才能返回对应的数据,而WebServer不能主动的推送数据给Browser/UserAgent。

最初这么设计HTTP协议的原因是,假设WebServer能主动的推送数据给Browser/UserAgent,那么Browser/UserAgent就太容易受到攻击了,一些广告商也会主动把广告在不经意间强行的传输给客户端,这不能不说是一个灾难。那么单向的HTTP协议给Web应用开发带哪些问题呢?

现在假设我们要开发一个基于Web的应用去获取当前WebServer的实时数据。例如股票实时行情、火车票剩余票数等。这就需要Browser/UserAgent与WebServer之间反复进行HTTP通信,Browser/UserAgent不断的发送请求去获取当前的实时数据。

常见的方式

  • Polling

Polling轮询是通过Browser/UserAgent定时向WebServer发送HTTP请求,WebServer收到请求后把最新的数据发回给Browser/UserAgent,Browser/UserAgent得到数据后将其显示,然后再定期重复此过程。

虽然这样可以满足需求,但仍存在问题,例如某段时间内WebServer没有更新的数据,但Browser/UserAgent仍然会定时发送请求过来询问,WebServer可以把以前的老数据再传送过去,Browser/UserAgent把这些没有变化的数据再显示出来。这样既浪费网络带宽,有浪费CPU利用率。

如果说把Browser/UserAgent发送请求的周期调大一些,就可以缓解这个问题,但如果WebServer的数据更新很快时,这样又不能保证Web应用获取数据的实时性。

  • LongPolling

LongPolling是对Polling的一种改进。

Browser/UserAgent发送HTTP请求到WebServer,此时WebServer可以做2件事情:

  1. 如果WebServer有新的数据需要传送,就立即把数据发回给Browser/UserAgent,Browser/UserAgent收到数据后,立即再发送HTTP请求给WebServer。
  2. 如果WebServer没有新数据需要传送,这里与Polling的方式不同的是,WebServer不是立即发送回应给Browser/UserAgent,而是将这个请求保持住,等待有新的数据来到,再去响应这个请求。当然,如果WebServer的数据长期没有更新,一段时间后,这个HTTP请求就会超时,Browser/UserAgent收到超时信息后,在立即发送一个新的HTTP请求给服务器,然后依次循环这个过程。

LongPolling的方式虽然在某种程度上减少了网络带宽和CPU利用率等问题,但仍存在缺陷。

例如WebServer的数据更新速度较快,WebServer在传送一个数据包给Browser/UserAgent后必须等待Browser的下一个HTTP请求到来,才能传递第二个更新的数据包给Browser。这样的话,Browser显示实时数据最快的时间为2 xRTT(往返时间)。另外在网络拥堵的情况下,这个应该是不能让用户接受的。另外,由于HTTP数据包的头部数据量很大(通常有400多个字节),但真正被服务器需要的数据却很少(有时只有10个字节左右),这样的数据包在网络上周期性传输,难免对网络带宽是一种浪费。

综上所述,要是在Browser有一种新的网路一些,能支持客户端和服务端的双向通信,而且协议的头部又不那么庞大就very nice了。WebSocket正是肩负这样的使命登上了Web的舞台。

WebSocket 原理

WebSocket是一种双向通信协议,它建立在TCP之上,同HTTP一样通过TCP来传输数据,但与HTTP最大不同的是:

  1. WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和Browser/UserAgent都能主动的向对象发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的简单模拟Socket的协议。
  2. WebSocket需要通过握手连接,类似TCP也需要客户端和服务端进行握手连接,连接成功后才能相互通信。

img

简单说明下WebSocket握手的过程

当Web应用端调用new WebSocket(url)接口时,Browser就开始了与地址为URL的WebServer建立握手连接的过程。

  1. Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用将收到错误消息通知。
  2. 在TCP建立连接成功后,Browser/UserAgent通过HTTP协议传送WebSocket支持的版本号、协议的字版本号、原始地址、主机地址等一系列字段给服务端。
  3. WebSocket服务器收到Browser/UserAgent发送来的握手请求后,如果数据包数据和格式正确,客户端和服务端的协议版本匹配等,就接受本次握手连接,并给出对应的数据回复,同样回复的数据包也是采用HTTP协议传输。
  4. Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口向服务器发送数据。否则,握手连接失败,Web应用会收到onerror消息,并且能知道连接失败的原因。

WebSocket与TCP、HTTP的关系

“三次握手,四次挥手”你真的懂吗?

客户端:是服务器吗?我要跟你通信,听得到我说话吗?
服务器:可以通信,你听得到我说话吗?
客户端:我也听得到。

是双工

三次握手和四次挥手TCP一定是客户端发起 (谁发请求谁就是客户端) (http是基于tcp连接的)

TCP滑动窗口https://www.bilibili.com/video/BV1PZ4y1P7zp

UDP报文格式:源端口+目标端口+报文长度+内容

TCP报文格式,一行4字节,1字节8位

16位源端口号 | 16位目的端口号

32位序号(seq 每个报文的序号,随机生成

32位确认序号 ack (服务器的ack=客户端发起请求的seq+1, 客户端的ack=服务器的seq+1

4位首部长度 | 保留6位 | URG | ACK(1位标志位) | PSH | RST | SYN(1位, 为1则为新) | FIN(我要结束,我要断开) | 16位窗口大小

image-20220710220737099

image-20220716200913805

image-20220716201845317

全文背诵:

面试官你好,这个问题我是知道的。TCP/IP协议是传输层的一个面向连接的安全可靠的传输协议。

三次握手的机制是为了保证能建立一个安全可靠的连接;第一次握手是由客户端发起,客户端会向服务端发送一个报文,报文里面SYN标志位是置1的,当服务端收到这个报文的时候就知道客户端要和我发起一个新的连接,于是服务端就向客户端发送一个确认消息包ACK位置1,以上两次握手之后,对于客户端而言,其实是已经知道了所有信息,就是我既能给服务端发送消息,我还能收到服务端的消息;对于服务端而言,两次握手是不够的,因为到目前为止,服务端只知道一件事情,就是客户端给我发送的消息我收的到,但是我发给客户端的消息,客户端能不能收到我还不知道。

所以还要进行第三次握手。第三次握手就是当客户端收到服务端发过来的确认消息的报文之后,还要继续给服务端进行一个回应,也是一个ACK位置1的一个确认消息。

通过以上三次连接,不管是服务端还是客户端都彼此知道了,我既能给对方发送消息也能收到对方的消息,那么这个连接就能被安全的建立了。

四次握手机制,也是由客户端(服务器也可以)首先发起的,客户端会发起一个报文,在报文里面FIN标志位置1;当服务端收到这报文之后,我就知道了客户端想要和我断开连接,但是此时服务端不一定能做好准备,因为当客户端发起断开连接的时候,对于服务端而言它极有可能有未发送完的的消息,它还要继续发送;所以此时对于服务端而言他只能进行一个消息确认,我先告诉对方,我知道你要和我断开连接了,但是我这还可能没有做好准备,你还需要等我一下,等会我会告诉你;于是,发完这个消息确认包后,可能稍作片刻,它可能会继续发送一个断开连接的报文,一个FIN位置1的报文,是由服务端发给客户端的,这个报文表示了服务端已经做好了断开连接的准备,那么当这个报文发给客户端的时候,客户端同样要给服务端继续发送一个消息确认的报文。一共有四次,通过这四次的相互沟通和连接,我就知道了,不管是服务端还是客户端都已经做好了断开连接的准备,于是连接就可以被断开了。

这是我对三次握手和四次挥手的理解。

image-20220710221248490

为什么服务器要分两次挥手,因为有可能正在发送一个数据给客户端,不能立刻断开。

server的第二步说:我知道你要和我断开,你先等我一下,我还没传输完。

server的第三步说:我终于传输完了,客户端你断开吧。

四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。
客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。
因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。

网络分层结构

开放系统互连参考模型 (Open System Interconnect 简称OSI)

image-20220711171534690

7、应用层:用户app里的数据,图片、声音、文字

6、表示层: 用bmp或jpeg编码表示图片数据,WAV或mp3编码表示声音数据,wmv或avi编码表示视频数据。

5、会话层:会话连接,建立两个应用软件之间的会话关系和连接。

4、传输层(数据段):简历TCP或UDP连接,建立端到端的连接。 给报文打上端口号,例如80是网页数据,4000是qq数据

3、网络层(数据包):给数据表打上ip地址,并且使用路由转发。使用的协议统一,网络层基于IP地址进行路由转发。进行数据传输:路由数据包,选择传递数据的最佳路径,支持逻辑寻址和路径选择。

2、数据链路层(数据帧):以太网协议,mac地址,访问介质:定义如何格式化数据以便传输以及访问网络,支持错误检测。

1、物理层(数据位):网线,二进制传输,定义了各种规范。

image-20220716100310633

image-20220710224132044

image-20220710224203925

image-20220710224218692

网络模型图

socket就是这里的TCP或UDP

image-20220710230633852

基于TCP/UDP协议的应用层协议有哪些?

1、基于TCP的应用层协议有:HTTP、FTP、SMTP、TELNET、SSH
协议 全称 默认端口
HTTP ( 浏览网页 ) HyperText Transfer Protocol(超文本传输协议) 80
FTP File Transfer Protocol (文件传输协议) 20用于传输数据,21用于传输控制信息
SMTP Simple Mail Transfer Protocol (简单邮件传输协议) 25
TELNET Teletype over the Network (远程登录,明码传输) 23

WebSocket 80或443端口丰富

SSH Secure Shell(远程,加密传输) 22 使用多种加密技术 保证在 用户终端和服务器之间 建立加密安全连接

对称加密是一种加密类型,在加密和解密时候使用同一个密钥。SSH数据传输时候基本上所有过程都是使用对称密钥来加密。只有在刚开始创建连接阶段和身份认证握手阶段才使用非对称加密。为了确保信息的安全传输,SSH 在事物中的多个地方采用了多种不同类型的数据操纵技术,包括对称加密,非对称加密以及哈希。

2、基于UDP的应用层协议:DNS、TFTP(简单文件传输协议)、SNMP:简单网络管理协议
协议 全称 默认端口
DNS Domain Name Service (域名服务) 53
TFTP Trivial File Transfer Protocol (简单文件传输协议) 69
SNMP Simple Network Management Protocol (简单网络管理协议) 通过UDP端口161接收,只有Trap信息采用UDP端口162。
NTP Network Time Protocol (网络时间协议) 123


网络层有ip协议、IGMP、ICMP协议

数据链路层有ethernet以太网、ppp、PPPoE协议。

TCP和UDP的区别

image-20220801211824291

传输控制协议TCP(Transmission Control Protocol)

用户数据包协议UDP(User Datagram Protocol)

  1. TCP是面向连接的,通过三次握手建立客户端和服务端之间的可靠连接,而UDP是面向无连接的,不需要建立连接就可以直接给多台机器发送数据。
  2. TCP保证可靠服务,会进行超时重传。而UDP不保证可靠交付。
  3. TCP是面向字节流的协议,字节流没有头尾,但是流通过报文段发送出去,UDP是面向报文的协议,通过数据报发送数据。
  4. TCP有流量控制(滑动窗口),拥塞控制(慢启动、拥塞避免、快重传、快恢复)等机制,而UDP只受数据生成的速率等影响,跟网络状况无关。
  5. TCP是点对点连接的(全双工),UDP支持一对一、一对多、多对一和多对多的连接。

UDP适合:直播,实时游戏,http3.0等场景。
不需要建立连接,一对一沟通,可以广播,对于丢包不敏感,网络较好的内网,即使网络不畅也不能降低数据发送的速度。

TCP适合发送文件 发送邮件 浏览网页

image-20220710224707044

image-20220710224734156

TCP面向字节流与UDP面向报文
之前对于tcp和udp只是记住了一个面向字节流,一个是面向报文的,但是并没有真正的理解

通俗的解释:

面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率。若太短,会是IP太小。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这也就是说,应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文。不会出现黏包的问题。

面向字节流的话,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。会出现黏包的问题。

浏览器输入URL返回页面的过程

  1. 首先解析域名,通过DNS查询,找到域名的IP地址
  2. 浏览器利用IP直接和网站主机通信,三次握手,建立TCP连接。浏览器会以一个随机端口向服务器的web程序80(HTTP是90端口,HTTPS是443端口)端口发起TCP的连接。
  3. 建立TCP连接吼,浏览器向主机发起一个HTTP请求。
  4. 服务器响应请求,返回响应数据。
  5. 浏览器解析响应内容,进行渲染,呈现给用户。
  6. HTTP请求结束,断开TCP连接。

深入理解DNS解析过程

DNS是基于UDP协议的应用层协议

  1. 检查浏览器缓存和本地hosts文件是否有网址记录。

  2. 主机先向其本地域名服务器进行递归查询

  3. 本地域名服务器采用迭代的查询,它先向根域名服务器查询

  4. 域名服务器告诉本地域名服务器,下一次应该查询的顶级域名服务器的IP地址。

  5. 本地域名服务器向顶级域名服务器发起查询。

  6. 顶级域名服务器告诉本地域名服务器,下一次应查询的权限域名服务器IP地址

  7. 本地域名服务器向权限域名服务器发起查询。

  8. 权限域名服务器告诉本地域名服务器所查询的IP地址。

  9. 本地域名服务器把查询结果告诉主机

    DNS域名解析过程-前端八股文

域名(Domain)

mail.ccav.com
---- ---- ----
三级域名 二级域名 顶级域名

域名服务器划分

了解域名服务器划分之后,就会对DNS解析的过程大概有些了解。域名服务器按照层级分为:

本地域名服务器

本地域名服务器相当于一个班主任,你有点啥事都找他。当一个主机发出DNS查询的时候,这个查询的请求就会发送到本地域名服务器。本地域名服务器一般由ISP(联通电信)管理。

根域名服务器 .

根域名服务器是最高层次的域名服务,是校长,只负责规划大方向。他知道所有顶级域名服务器的域名和IP地址。不管那个本地域名服务器若自己不能不能解析,那首先请求的就是根域名服务器。根域名服务器不会把待查询的域名直接转换成IP,而是告诉本地域名服务器下一步应该找哪一个顶级域名服务器进行查询。

顶级域名服务器 com cn net gov

顶级域名服务器是负责各个方向的副校长,有负责安全的,有负责教学的。他负责管理该顶级域名下的所有二级域名,当收到DNS查询请求后就会给出响应的应答,可能是最终的结果,也可能是下一步应该找到的域名服务器的IP地址。

权限域名服务器 bilibili qq baidu

权限域名服务器就是负责一个区的域名服务器,是基层干活的,比如宿管,各科老师,他负责一个更小的区域。当一个权限服务还不能给出最后的回答时,就会告诉查询请求的DNS客户,下一步应该找哪个权限域名服务器。

再下还有主机名 www mail member space

域名查询的两种方式

递归查询

递归查询,就是你找我要一个域名的IP地址,但是我不知道,那我去帮你去向知道的人问这个IP地址。举个例子就是,你问你班主任怎么做番茄炒鸡蛋,你班主任不知道,但是你班主任去问了食堂厨师,然后告诉你。这就叫递归查询。
从host到本地域名服务器一般是用的递归查询。

迭代查询

迭代查询就是,你找我要一个域名的IP地址,我也不知道这个IP地址,但是我知道谁知道,我告诉你去找谁问。举个例子就是,你们班主任到了食堂随便拉了个人问怎么做番茄炒鸡蛋,那个人说,我只是个卖饭的,我不知道,但是我知道A君是在后厨的,你可以去问A君。然后你班主任就去问A君了。呐,这就叫迭代查询。
从本地域名服务器到根域名服务器一般是用的迭代查询。

session

image-20220710224430637

image-20220710224459231

image-20220710224528356

协议

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

为了使数据在网络上从源到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议(protocol),它最终体现为在网络上传输的数据包的格式。

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

image-20220716101810317

image-20220716101526161

UDP协议

image-20220716101848093

image-20220716102012808

TCP协议

image-20220716101939094

IP协议

image-20220716102105919

image-20220716102210270

以太网帧协议

image-20220716102501564

ARP协议

image-20220716102517324

封装

image-20220716102601333

分用

image-20220716102856437

image-20220716102903822

image-20220716102914332

image-20220716103540290

image-20220716103451453

如何得到目标端的mac地址,arp请求会发给所有局域网内

image-20220716103647487

image-20220716102501564

字节序(同序为大端)

image-20220716112033084

image-20220716111515515

image-20220716111945752

image-20220716112003404

网络字节序一定是大端,主机字节序不一定。

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。

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

BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。

#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); // 主机字节序 - 网络字节序

image-20220716114523015

image-20220716115324918

image-20220716115609326

image-20220716115408917

image-20220716115654574

image-20220716115705028

Socket地址

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

通用 socket 地址

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

#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;//sa_family 成员是地址族类型(sa_family_t)的变量
char sa_data[14];
};
typedef unsigned short int sa_family_t;

sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:

image-20220716120025410

image-20220716120117276

image-20220716152431277

#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)];
};

struct in_addr
{
in_addr_t s_addr;
};

struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};

typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr

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

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 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 是一样的

image-20220716155600862

image-20220716155541442

image-20220716155835353

用socket实现网络通信

image-20220716155939527

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

套接字Socket函数

#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 通信的服务器端

#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;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
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("clinet closed...");
break;
}

char * data = "hello,i am server";
// 给客户端发送数据
write(cfd, data, strlen(data));
}

// 关闭文件描述符
close(cfd);
close(lfd);

return 0;
}
// TCP通信的客户端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

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

// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.193.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};
while(1) {

char * data = "hello,i am client";
// 给客户端发送数据
write(fd, data , strlen(data));

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滑动窗口

滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。

TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0时,发送方一般不能再发送数据报。

滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。

窗口理解为缓冲器的大小,会随着发送数据和接收数据而变化。通信双方都有发送和接收数据的缓冲区。

image-20220716202339331

# mss: Maximum Segment Size(一条数据的最大的数据量)
# win: 滑动窗口,相当于空闲的大小
1. 客户端向服务器发起连接,客户端的滑动窗口是4096,一次发送的最大数据量是1460
2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
3. 第三次握手
4. 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
5.10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗
口大小是2k = 剩余2k空间
6.11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗
口大小是4k,又处理2k,还剩4k空间
7.12次,客户端给服务器发送了1k的数据
8.13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
9.14次,服务器回复ACK 8194, a:同意断开连接的请求 b:告诉客户端已经接受到方才发的2k的数据
c:滑动窗口2k
10.1516次,通知客户端滑动窗口的大小
11.17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12.18次,第四次挥手,客户端同意了服务器端的断开请求

image-20220716202752580

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


TCP通信并发 - 多进程实现并发服务器

要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。
思路:
1. 一个父进程,多个子进程
2.父进程负责等待并接受客户端的连接
3.子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信。
// 多进程 服务器端   
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

void recyleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
}else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0){
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}

int main() {

struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);


// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}

// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}

// 不断循环等待客户端连接
while(1) {

struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) {
continue;
}
perror("accept");
exit(-1);
}

// 每一个连接进来,创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(cfd, &recvBuf, sizeof(recvBuf));

if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
exit(0); // 退出当前子进程
}

}
close(lfd);
return 0;
}

多线程实现并发服务器

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid; // 线程号
};

struct sockInfo sockinfos[128];

void * working(void * arg) {
// 子线程和客户端通信 cfd 客户端的信息 线程号
// 获取客户端的信息
struct sockInfo * pinfo = (struct sockInfo *)arg;

char cliIp[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
}
close(pinfo->fd);
return NULL;
}

int main() {

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}

// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}

// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}

// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1) {

struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

struct sockInfo * pinfo;
for(int i = 0; i < max; i++) {
// 从这个数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {
sleep(1);
i--;
}
}

pinfo->fd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);

// 创建子线程
pthread_create(&pinfo->tid, NULL, working, pinfo);

pthread_detach(pinfo->tid);
}

close(lfd);
return 0;
}