基于TCP协议的socket通信

Posted tutougold

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于TCP协议的socket通信相关的知识,希望对你有一定的参考价值。

基于TCP协议的socket通信

1.基于TCP协议的socket循环通信

服务端:
    
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

phone.bind(('127.0.0.1',8080))

phone.listen(5)


conn, addr = phone.accept()
print(conn,addr)

while 1:  # 循环收发消息
    try:
        from_client_data = conn.recv(1024)
        if from_client_data.upper() == b'Q':
            print("客户端正常退出..")
            break
        print(from_client_data.decode('utf-8'))
        to_client_data = input(">>>").strip().encode('utf-8')
        conn.send(to_client_data)
    
    except ConnectionResetError:
        break

conn.close()
phone.close()
客户端:
    
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话

phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号


while 1:  # 循环收发消息
    client_data = input('>>>').strip().encode('utf-8')
    if not client_data:
        print("发送内容不能为空")
        continue
#服务端接收到空的内容,就会一直处于阻塞状态,所以无论哪一端,都不能为空发送
    phone.send(client_data)
    if client_data.upper() == b'Q':
        break
    from_server_data = phone.recv(1024)
    
    print(from_server_data.decode('utf-8'))

phone.close()  # 挂电话




bytes类型:
    ASCII字符,在字符串前面加b,b""
    非ASCII字符,encode转化为buytes类型

2.基于TCP协议的socket连接循环通信

服务端:
    
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

phone.bind(('127.0.0.1',8080))

phone.listen(5)

while 1 : # 循环连接客户端
    conn,addr = phone.accept()
    print(addr)
    
    while 1:
        try:
            from_client_data = conn.recv(1024)
            if from_client_data.upper() == b'Q':
                print("客户端正常退出..")
                break
            print(from_client_data.decode('utf-8'))
            to_client_data = input(">>>").strip().encode('utf-8')
            conn.send(to_client_data)
        
        except ConnectionResetError:
            break

    conn.close()
phone.close()
客户端(多个):
    
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话

phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号


while 1:
    client_data = input('>>>')
    if not client_data:
        print("发送内容不能为空")
        continue
#服务端接收到空的内容,就会一直处于阻塞状态,所以无论哪一端,都不能为空发送
    phone.send(client_data.encode('utf-8'))
    if client_data.upper() == b'Q':
        break
    from_server_data = phone.recv(1024)
    
    print(from_server_data.decode('utf-8'))

phone.close()  # 挂电话

3.基于TCP协议的socket通信:远程执行命令

服务端:
    
import socket
import subprocess

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

phone.bind(('127.0.0.1',8080))

phone.listen(2)
# listen: 2 允许有两个客户端加到半链接池,超过两个则会报错

while 1 : # 循环连接客户端
    conn, addr = phone.accept()
    print(addr)
    
    while 1:
        try:
            cmd = conn.recv(1024)
            if cmd.upper() == b'Q':
                print('客户端正常退出聊天了')
                break
            ret = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            result = ret.stdout.read() + ret.stderr.read()
            conn.send(result)
        except ConnectionResetError:
            print("客户端连接中断")
            break

    conn.close()
phone.close()



# shell: 命令解释器,相当于调用cmd 执行指定的命令。
# stdout:正确结果丢到管道中。
# stderr:错了丢到另一个管道中。
# windows操作系统的默认编码是gbk编码。
客户端:
    
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话

phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号


while 1:
    cmd = input('>>>')
    if not cmd:
        print("发送内容不能为空")
        continue
#服务端接收到空的内容,就会一直处于阻塞状态,所以无论哪一端,都不能为空发送
    phone.send(cmd.encode('utf-8'))
    if client_data.upper() == b'Q':
        break
    
    from_server_data = phone.recv(1024)
    
    print(from_server_data.decode('gbk'))

phone.close()  # 挂电话


4.粘包现象

什么是粘包?
TCP粘包:socket读取时,读到了实际意义上的两个或多个数据包的内容,同时将其作为一个数据包进行处理。

只有TCP有粘包现象,UDP永远不会粘包.
具体原因:
    发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

5.操作系统的缓冲区

技术图片

socket缓冲区:是指你在进行socket通信的时候,收发命令时的一个中间存放命令的存储空间,一般系统设置为8k,可以自己调整大小.
    
为什么存在缓冲区:
    1.暂时性的存储一些数据
    2.如果出现网络波动,缓冲区的存在可以保证数据的匀速,稳定收发
socket缓冲区的详细解释:
    
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

这些I/O缓冲区特性可整理如下:

1.I/O缓冲区在每个TCP套接字中单独存在;
2.I/O缓冲区在创建套接字时自动生成;
3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4.关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

1.unsigned optVal;
2.int optLen = sizeof(int);
3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);
4.printf("Buffer length: %d\\n", optVal);


代码查看缓冲区大小:
import socket
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 重用ip地址和端口
server.bind(('127.0.0.1',8010))
server.listen(3)
print(server.getsockopt(socket.SOL_SOCKET,socket.SO_SNDBUF))  # 输出缓冲区大小
print(server.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF))  # 输入缓冲区大小

5.出现粘包现象的情况

  • 1.接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只接收了一小部分,服务端下次再接收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

    服务端:
    import socket
    import subprocess
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1', 8080))
    
    phone.listen(5)
    
    while 1:  # 循环连接客户端
        conn, client_addr = phone.accept()
        print(client_addr)
    
        while 1:
            try:
                cmd = conn.recv(1024)
                ret = subprocess.Popen(
                    cmd.decode('utf-8'),
                    shell=True, 
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE)
                correct_msg = ret.stdout.read()
                error_msg = ret.stderr.read()
                conn.send(correct_msg + error_msg)
            except ConnectionResetError:
                break
    
      conn.close()
    phone.close()
    
    
    
    客户端:
    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
    
    phone.connect(('127.0.0.1',8080))  # 与客户端建立连接, 拨号
    
    
    while 1:
        cmd = input('>>>')
        phone.send(cmd.encode('utf-8'))
    
        from_server_data = phone.recv(1024)
    
        print(from_server_data.decode('gbk'))
    
    phone.close() 
    
    # 由于客户端发的命令获取的结果大小已经超过1024,那么下次在输入命令,会继续取上次残留到缓存区的数据。    
    
  • 2.发送端需要等待缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会将数据合到一起,产生粘包)

    服务端:
    
    import socket
    
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.bind(('127.0.0.1', 8080))
    
    phone.listen(5)
    
    conn, client_addr = phone.accept()
    
    frist_data = conn.recv(1024)
    print('1:',frist_data.decode('utf-8'))  # 1: helloworld
    second_data = conn.recv(1024)
    print('2:',second_data.decode('utf-8'))
    
    
    conn.close()
    phone.close()    
    
    
    客户端:
    
    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
    
    phone.connect(('127.0.0.1', 8080)) 
    
    phone.send(b'hello')
    phone.send(b'world')
    
    phone.close()  
    
    # 两次返送信息时间间隔太短,数据小,造成服务端一次收取

6.解决粘包现象

struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes

import struct
# 将一个数字转化成等长度的bytes类型。
ret = struct.pack('i', 183346)
print(ret, type(ret), len(ret))

# 通过unpack反解回来
ret1 = struct.unpack('i',ret)[0]
print(ret1, type(ret1))


# 但是通过struct 处理不能处理太大

ret = struct.pack('l', 4323241232132324)
print(ret, type(ret), len(ret))  # 报错
low版:
#问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕如何让发送端在发送数据前,把自己将要发送的字节流总数按照固定字节发送给接收端后面跟上总数据,然后接收端先接收固定字节的总字节流,再来一个循环接收完所有数据。
#由于总数据的长度转化成的字节数不固定,所以引入struct模块
    
服务端:
import socket
import subprocess
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

phone.bind(('127.0.0.1', 8080))

phone.listen(5)

while 1:
    conn, client_addr = phone.accept()
    print(client_addr)
 
    while 1:
        try:
            cmd = conn.recv(1024)
            ret = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            result = ret.stdout.read() + ret.stderr.read()
            total_size = len(result)
            print(f"总字节数total_size")
            
            # 1.制作固定报头
            header = struct.pack('i', total_size)
            
            # 2.发送报头
            conn.send(header)
            
            # 3.发送真实数据:
            conn.send(result)

        except ConnectionResetError:
            break

    conn.close()
phone.close()

# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。



客户端:

import socket
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

phone.connect(('127.0.0.1',8080))


while 1:
    cmd = input('>>>').strip()
    if not cmd: 
        continue
    phone.send(cmd.encode('utf-8'))
    
    # 1,接收固定报头
    header = phone.recv(4)
    
    # 2,解析报头
    total_size = struct.unpack('i', header)[0]
    
    # 3,根据报头信息,接收真实数据
    total_data = b''
    
    while len(total_data) < total_size:
        total_data += phone.recv(1024)
        
    print(len(total_data))
    print(total_data.decode('gbk'))

phone.close()
帮助理解:
    
TCP粘包:socket读取时,读到了实际意义上的两个或多个数据包的内容,同时将其作为一个数据包进行处理。
TCP拆包:socket读取时,没有完整地读取一个数据包,只读取一部分。

粘包/拆包问题一般的处理方式有四种:

1.数据段定长处理,位数不足的空位补齐。
2.消息头+消息体,消息头中一般会包含消息体的长度,消息类型等信息,消息体为实际数据体。
3.特殊字符(如:回车符)作为消息数据的结尾,以实现消息数据的分段。
4.复杂的应用层协议,这种方式使用的相对较少,耦合了网络层与应用层。 
上面的四种方式目的都是为了将数据在流中精确分开以便进一步解析处理,在自定义的协议中,第二种方式用的比较多,因为它更能满足定制化协议开发需求,比如自定义Netty协议时可以将具体数据报文放入消息体,消息头中根据需要放入其他变量(如:消息类型,此处可以具体对应到维护netty长链接的心跳消息、客户端请求消息、服务端处理结果消息等)。



阻塞模式
对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:

1.首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。
2.如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
3.如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
4.直到所有数据被写入缓冲区 write()/send() 才能返回。

当使用 read()/recv() 读取数据时:

1.首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
2.如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
3.直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。TCP套接字默认情况下是阻塞模式,也是最常用的。当然你也可以更改为非阻塞模式。

以上是关于基于TCP协议的socket通信的主要内容,如果未能解决你的问题,请参考以下文章

基于tcp的socket通信

一个基于TCP协议的Socket通信实例

基于TCP协议的socket通信

什么是 socket?简述基于 tcp 协议的套接字通信流程?

基于套接字通信(tcp)

基于TCP与UDP协议的socket通信