深入理解Socket套接字原理

Socket套接字原理

1、什么是Socket

在计算机领域,套接字Socket作为计算机之间进行通信的固定的约定方式之一存在。这种太抽象了,我举个例子,我们要是用笔记本电脑前需要先对电脑供电,那供电就有两种方式电线插座供电和电池供电,电网有电就用插座供电,电网没电就用笔记本的自带的电池供电。那么这个供电的工具(电池或者电线插座)就是套接字Socket

Socket起源于Linux系统 ,我们都知道Linux是以文件系统进行存在。在linux里万物皆文件,就连进程都成为了一种数据结构,设备也被视为一个文件可以读写。在我看来Socket对于计算机就是实现这种模式的工具,Socket的函数就是实现对通信的操作来实现网络通信。

Socket作为中间件几乎支持所有协议使用

2、Socket如何实现网络通信

学过计网的都知道,对于计算机本地通信之间的通信是由进程实现的。

2.1、本地进程的通信方式

常见就只有几种。消息队列、管道、进程管理器、信号量等

  1. 消息队列 ,按照普通队列方式完成多进程之间的数据传递(multiprocessing模块的Queue)

  2. 管道 ,文件的一种,本质是一个固定大小的缓冲区,在Linux中缓冲区大小为4kb。

    管道工作原理:假设两个进程之间需要通讯,前面的进程对缓冲区进行文件写操作,后面的进程对文件进行读操作,就这样前面写后面读,两个进程就通过管道完成通信。

    #这里提供一个演示文本
    import datetime
    from  multiprocessing import Process,Pipe
    import time
    
    from charset_normalizer import constant
    
    num=0
    def sendMsg(p):
        now=str(datetime.datetime.now())
        # p.name
        msg="管道传输的数据"
        # name=psutil.Process(os.getpid()).name()
        print("这个是独立子进程 "+now+"数据是 :"+msg)
        p.send(msg)
        print("数据发送成功")
        p.close()
    
    
    def receiveMsg(p):
        print("接受管道传输信息:",p.recv())
    
    if __name__=='__main__':
        print("-----------start---------")
        # (grandPipe,parentPipe)=Pipe()
        (parentPipe,sonPipe)=Pipe()
    
        task1=Process(target=sendMsg,args=[parentPipe])
        task1.start()
        task3=Process(target=sendMsg,args=[parentPipe],name="task1")
        task3.start()
        task2=Process(target=receiveMsg,args=[sonPipe],name="task2")
        task2.start()
    
    
        time.sleep(5)
        print("-----------end---------")
    
  3. 远程过程调用、共享内存、同步等

2.2、网络中进程的通信方式

要理解网络进程通讯,我们要先解决两个问题

  1. 如何标定一台主机,我们需要直到需要通信的进程在哪一个主机上运行?
  2. 如何标定唯一进程,本地进程通讯可以通过pid区别,在网络上怎么区别?

TCP/IP协议族会解决第一个问题,定位通讯主机;而网络七层的传输层利用三元组(ip、端口、协议)则可以解决第二个问题

2.3、Socket实现通信的过程

现在我们已经知道网络中进程如何通信的,那么如何去实现他就是Socket的作用了。

Socket就如定义一般,是作为三元组解决网络通信中的中间件工具,就目前程序来说大部分都是采用Socket套接字中间件去实现网络中进程的通讯

Socket通信的传输方式

  1. SOCK_STREAM ,面向连接数据传输方式,数据可以准确无误达到另外一台计算机。如有丢失损坏,允许重新发送,但是效率慢。Http协议使用的就是该套接字进行传输的,毕竟浏览器要解析网页,数据就不能出现错误的地方
  2. SOCK_DGRAM , 表示面向无连接的数据传输方式,不做数据校验。如果数据在传输过程中损坏,没有办法补救,错了就是错了。我们使用qq和微信的时候使用的就是该种套接字

3、加入套接字后去理解TCP/IP

TCP协议作为一种面向连接的可靠字节流通信协议,数据在传输前要建立连接,建立连接传输后还要断开连接。用TCP建立连接目的就是保证ip、端口和物理线路正确开辟传输通道,

我们建立连接三次握手通过套接字可以抽象成如下场景

1、客户端发送建立连接请求发送SYN报文,套接字A向套接字B表示A要连接传送数据

2、服务端接受SYN报文返回客户端ACk确认报文和SYN报文,套接字B表示我已经就绪

3、客户端接受ACK报文然后向服务端发送ACK确认报文,套接字A表示感谢B的支持

对于TCP四次握手断开连接

TCP断开连接是可以通过关闭套接字来实现断开连接

1、客户端发送FIN报文,套接字B向套接字A表示我要断开连接

2、服务端接受FIn后向客户端发送ACK确认报文,套接字A向套接字B表示我知道了正在处理

3、服务端完成断开连接处理后向客户端发送Fin终止报文,套接字A向套接字B表示我已处理好断开连接操作B你可以断开了

4、客户端接受FIN后再向服务端发送ACK确认报文,套接字B向套接字A表示好的我会断开连接然后等待数秒断开连接

3.1、TCP使用Socket所导致的粘包现象

什么是粘包

多个要发送的数据包被联系存储与连续的缓存中,如果接收方对发过来的数据进行读取时没能确定发送方的发送边界(连续几个数据包为一个完整数据没法确定),而是采用估测方式假定边界值(发送方认为5个数据包为一个完整数据,而接收方认为6个包为一个完整数据),最后就会导致发送方发送的多个数据包在接收方处被视为一个数据包

为什么需要考虑粘包

我们假设发送方需要像接受方发送两条信息

hello this message、you can not use this message,如果发生粘包现象发生,就会出现hello this messageyoucan not use this message,这样接收方就不知道这个信息是什么意思,处理不了。

出现粘包的原因

出现粘包的原因发送方和接收方都有责任,

  • 发送方的粘包原因

    发送方主责是TCP协议本身所造成的,TCP作为注重传输效率的协议,发送前会收集一定量的数据才能去发送。若是连续几次的发送的数据量变少,之后的传输中TCP的优化算法会把类似这种情况的数据都合成一包数据发送出去,不会考虑后果**(毕竟TCP协议只是一个打工人只考虑效益**),这就会导致接收方收到的粘连数据

  • 接收方的粘包原因

    接收方粘包主责在于使用此连接用户进程没有即时处理数据包。接收方一般接收数据后会先将数据存往一个缓冲区,使用连接的用户进程从缓冲区读取数据。 若是进程读取速度过慢,上一包和下一包数据会一起在缓冲区存储着,如果用户进程这个时候去读,就会获得多包数据造成粘连

如何解决粘包现象

  • 如果是发送发导致粘包现象,用户可以通过编程设置避免。TCP提供push强推送指令,一旦接受就立刻发出缓冲区数据,而不是等待缓冲区充满。
  • 对于接收方导致的粘包现象,用户可以通过优化程序设计、精简接受进程的工作量提高数据发送的优先级来避免粘包
  • 或者发送时机选择由接收方控制,人为的进行多次接收数据然后合并关联数据避免粘包

以上三种解决粘包现象的方法都有缺点,要么影响程序执行性能要么影响发送效率,或者在实际业务场景中不实用。

3.2、套接字如何断开TCP连接

套接字关闭是通过程序调用函数直接将套接字描述符从内存清除,然后再也不使用该套接字。套接字关闭后,通过该套接字的连接和缓存区自然失去了连接意义,TCP协议检测到套接字消失会自动执行关闭连接的操作。

默认情况下close()/closesocket()是调用关闭套接字的函数,shutdown()则是用来关闭连接的。

  • close()/closesocket(),调用即关闭套接字即向对端发送断开连接FIN包,所以会丢失缓存区的数据。
  • shutdown(),用来关闭TCP连接,会等到缓存区没有数据包内容时执行断开连接操作。

对于程序来说无论是调用close()/closesocket()还是shutdown(),计算机都会向对端发送FIN包请求断开连接。

4、导入套接字后理解OSI七层模型

在这里插入图片描述

5、Socket套接字函数与原理(基于python3.8)

Socket套接字是建立连接的中间件,而连接成立的前提就是有两台机器存在,所以Socket的使用方式也存在两种。一般这两端是服务器端和客户端。

同时两端创建socket时需要指定三种元素,ip地址协议类型、传输方式和使用的协议,其中协议指定为非必要,不指定默认是TCP

5.1、服务器端
  • 创建socket套接字的,socket()

  • 对socket绑定端口号和地址的,bind(),端口号和地址作为一组数据传入bind

  • 表示开始监听对端通讯的,listen()

    listen只是让套接字出于监听状态,要接受请求的话还得执行accept

  • 被动接受TCP客户端连接请求的,accept()

  • 从请求中接受数据指定接受量的,recv()

  • 关闭同时停止当前socket的,close()

5.2、客户端
  • 创建socket的,socket()
  • 主动连接指定地址与端口的,connect()
  • 向套接字中写入数据进行发送到套接字进行发送的,send()、sendall()
  • 关闭本端socket使用的,close()

5.3、扩展

socket本身作为一个中间件也支持udp协议,具体流程与TCP类似主要是服务器端和客户端用于数据处理的函数被替换为recvfrom()和sendto()

除此之外还有一些返回内容

s.getpeername()返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname()返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value)设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen])返回套接字选项的值。
s.settimeout(timeout)设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout()返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno()返回套接字的文件描述符。
s.setblocking(flag)如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile()创建一个与该套接字相关连的文件

6、用代码实现套接字Socket的实例

python3.8,我们可以用代码模拟socket工作流程

#socketServerTest.py 服务端代码

import socket


def function():
    s1=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #选择使用ipv4地址方式以及流模式传递数据,如果是ipv6则对应AF_INET6
 
    # host=socket.gethostname()
    # port=6999
    # # print(host)
    s1.bind(('LAPTOP-SRMA7P3L',6999))
    s2.bind(('LAPTOP-SRMA7P3L',6998))#这是绑定要监听的接口,我通过gethost获取到本机计算机地址
    s1.listen(5)#开始监听,允许使用五个连接排队
    s2.listen(2)
    while True:
        c1,addr1=s1.accept()#服务器端等待连接
        c2,addr2=s2.accept()
        # print(c1,addr1)
        while True:
            try:
                data1 = c1.recv(1024)#接受客户端数据
                data2 = c2.recv(1024)
                print('reieve ',data1.decode(),data2.decode())#打印数据
                c1.send(data1.upper())#发送数据
                c2.send(data2.upper())
            except ConnectionError as e:
                print('关闭正在展现连接')
                break
        c1.close()
        c2.close()
if __name__=='__main__':
    function()
#socketClientTest01.py 第一个客户端代码
import socket

def function():
    client =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(('LAPTOP-SRMA7P3L',6999))
    while True:
        msg='clinet1Msg'
        client.send(msg.encode('utf-8'))
        data=client.recv(1024)
        print('recv: ',data.decode())
    client.close()
if __name__ == '__main__':
    function()
    

#socketClientTest02.py 第二个客户端代码
import socket

def function():
    client =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(('LAPTOP-SRMA7P3L',6998))
    while True:
        msg='clinet2Msg'
        client.send(msg.encode('utf-8'))
        data=client.recv(1024)
        print('recv: ',data.decode())
    client.close()
if __name__ == '__main__':
    function()

都运行起来后服务端控制台会打印如下内容

reieve clinet1Msg clinet2Msg

参考文章

Socket粘包问题_blingpro的博客-CSDN博客

面向连接和无连接的套接字到底有什么区别 (biancheng.net)

什么是粘包?socket 中造成粘包的原因是什么? 粘包的处理方式_Nice07的博客-CSDN博客_什么是粘包

进程间通信的几种方式_古越少年的博客-CSDN博客_进程间的通信方式三种

一、Socket技术详解 - 简书 (jianshu.com)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值