Python3_socket编程

1、C/S 架构和socket的关系

  • socket就是为了完成C/S架构的开发
  • 用socket来做一个服务端(客户端)分别运行在不同的机器上

2、OSI七层协议

  • 应用层 ==> 提供应用软件的接口,以设置于另以软件之间的通信(有http、https、ftp、ssh等协议)
  • 表达层 ==> 把数据转换为能与接收者系统兼容的传输格式
  • 会话层 ==> 负责在数据传输中设置和维护计算机网络中两台计算机之间的通信
  • 传输层 ==> 把传输表头加上数据形式成数据包,包括了所有使用协议等发送信息,提供一个端口
  • 网络层 ==> 决定数据的路径选择和转寄,将网络表头加至数据包,以形成分组
  • 数据链路层 ==> 负责网络的寻址、错误侦测和改错;当表头和表尾被加至数据包时会形成帧
  • 物理层 ==> 负责计算机通信设备和网络媒体之间的互通

3、socket是什么

socket是在应用层与TCP/IP协议族通信的中间软件抽象层;本质就是一个接口,把复杂的TCP/IP协议隐藏在socket接口后面。

image.png

4、套接字

套接字课分为文件套接字和网络套接字:

  • 文件套接字:一台机器上不同程序之间的通信都是基于底层文件系统(AF_UNIX)
  • 网络套接字:还是两个程序进行通信,但依托的媒介是网络(AF_INET)

5、套接字的工作流程

image.png

6、socket基本用法

服务端:

import socket

host = '127.0.0.1'
port = 8080
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)       # 创建一个基于网络通信的TCP协议的socket对象
server.bind((host, port))
server.listen(5)    # 5表示最大的同时连接数

conn,addr = server.accept()  # conn表示链接;addr表示地址;返回的结果是一个元组
msg = conn.recv(1024)   # 接受信息,1024表示接收1024个字节的信息
print("客户端发来的消息是:%s" %msg.decode('utf-8'))
conn.send(msg.upper())  # 发送的消息

# 断开链接
conn.close()
server.close()

客户端:

import socket

host = '127.0.0.1'
port = 8080
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))
msg = 'hello'
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print("服务的发来的消息:%s" %data)
client.close()

7、socket底层原理

image.png

我们知道在TCP协议下如果client端和server端建立连接的话需要经历三次握手:

  • 如果client想要建立连接到server端的连接,client会向server发送一个SYN请求
  • 在server端收到SYN请求后会返回一个ACK,在原SYN值的基础上加上1,表示同意建立连接;并且同时还会向client发送一个SYN请求
  • 在client端接收到server发送过来的SYN请求后也会返回一个ACK,在该SYN的基础上加上1。至此client端和server端的连接建立完成

现在我们对应到socket编程上来会发现这三次握手是在 accept() 这一步上建立的

这里还需要补充概念叫的一个是Tcp SYN flood(TCP洪水攻击),其原理是在客户端向服务端发送连接请求并且服务端同意客户端进行连接后客户端并没有向服务端发送ACK;这时如果黑客使用1万个客户端向服务端发送请求并不发送最后的确认ACK包的话会严重影响到服务器的内存和带宽;这时又引出一个backlog(连接队列)参数,服务器会将处于半连接的TCP连接放入连接队列中,所以backlog参数对应到socket编程上来就是listen()的参数。


连接建立后的数据传输就相对简单许多了,客户端向服务器发送一条数据,服务端接收到该数据后会向客户端返回一个ACK包,表示已经就收到该数据,这就是为什么我们称为TCP为可靠传输协议。

数据的传输对应到socket编程上的话分别是recv() 和 send()两步。


当数据传输完成后会进行四次挥手进行连接的断开;如果客户端的数据先发完的话客户端就会向服务端发送FIN包,请求断开客户端到服务端的连接,这是服务端或返回一个ACK包,同意断开客户端到服务端的连接;接下去到服务端发送完数据后也会向客户端发送一个FIN包请求断开服务端到客户端的连接,这是客户端或返回一个ACK包,同意断开服务端到客户端的连接;至此服务端到客户端的连接和客户端到服务端的连接均已断开。

那么为什么建立连接只需要3次握手,而断开连接需要4次挥手呢?这是因为断开连接的前提是数据发送的完成,如果将服务端的ACK包和FIN包同时发送的话就无法确保两端的数据传输均已完成。

8、socket编程的收发消息原理

服务端

from socket import *

ipaddr = '127.0.0.1'
port = 8000
back_log = 5

tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.bind((ipaddr, port))
tcp_server.listen(back_log)

while True:
    conn,addr = tcp_server.accept()

    while True:
        try:
            data = conn.recv(1024)
            print("data is %s" %data.decode('utf-8'))
            conn.send(data.upper())
        except Exception:
            break

    conn.close()
tcp_server.close()

客户端

from socket import *

ipaddr = '127.0.0.1'
port = 8000

tcp_client = socket(AF_INET,SOCK_STREAM)

tcp_client.connect((ipaddr, port))

while True:
    msg = input(">>>: ")
    tcp_client.send(msg.encode("utf-8"))
    data = tcp_client.recv(1024)
    print("data is %s" %data.decode("utf-8"))
    
tcp_client.close()

这时我们如果在客户端运行程序,在输入的时候直接回车,即输入一个空值,这时候你就会发现客户端和服务端都卡在这里,服务端没有收到消息,客户端也没有收到消息;这是因为程序收发都是通过内核态来进行的;当程序(程序是在用户态中)通过系统能够调用内核态中的相应资源时才会将内容发聩给用户,即接收成功;同理当程序发送消息给其他机器时也是通过系统的处理来交给内核态通过网卡进行发送的。在上面的例子中空值是无法通过网络进行传输的,所以服务端的内核态中的资源为空,这就会导致服务端上的程序无法在内核态中找到相应的内容,程序当然也就无法进行下去。

注:在Linux下当客户端断开和服务端的连接的时候,服务端会一直接收一个空值,从而会陷入一个死循环中;这时我们需要使用一个if判断来结束该循环。如:

#!/usr/bin/python3
#-*- conding: utf-8-

from socket import *

ipaddr = '192.168.16.148'
port = 8000
back_log = 5

tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.bind((ipaddr, port))
tcp_server.listen(back_log)

while True:
    conn,addr = tcp_server.accept()
    while True:
        data = conn.recv(1024)
        if not data:
            break
        print("data is %s" %data.decode('utf-8'))
        conn.send(data.upper())

    conn.close()
tcp_server.close()

9、基于UDP的Socket编程

服务端

#!/usr/bin/python3
#-*- conding: utf-8-

from socket import *

ipaddr = '192.168.16.148'
port = 8080
recv_size = 1024

udp_server = socket(AF_INET, SOCK_DGRAM)
udp_server.bind((ipaddr, port))

data,addr = udp_server.recvfrom(recv_size)
print(data)
print(addr)

客户端

#!/usr/bin/python3
#-*- conding: utf-8-

from socket import *

ipaddr = '192.168.16.148'
port = 8080

udp_client = socket(AF_INET, SOCK_DGRAM)

msg = input(">>: ")
udp_client.sendto(msg.encode("utf-8"), (ipaddr, port))

10、使用TCP实现SSH功能

服务端

#!/usr/bin/python3
#-*- conding: utf-8-

import socket
import subprocess

ipaddr = '192.168.16.148'
port = 8000
back_log = 5
receive_size = 1024

ssh_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssh_server.bind((ipaddr, port))
ssh_server.listen(back_log)

while True:
    conn,addr = ssh_server.accept()
    while True:
        cmd = conn.recv(receive_size)
        if not cmd: break    # 当客户端发送的命令为空的时候结束当前的循环
        # print(cmd)
        # 使用subprocess模块运行客户端发送的命令
        res_cmd = subprocess.Popen(cmd, shell=True, 
                stdin = subprocess.PIPE, 
                stdout = subprocess.PIPE, 
                stderr = subprocess.PIPE)
        err_cmd = res_cmd.stderr.read()
        # 当客户端发送的命令运行错误的时候
        if err_cmd:
            conn.send("Command Error".encode("utf-8"))
            continue
        # 当客户端发送的命令运行正常的时候
        else:
            out_cmd = res_cmd.stdout.read()

        conn.send(out_cmd)
    conn.close()

客户端

#!/usr/bin/python3
#-*- conding: utf-8-


import socket

ipaddr = '192.168.16.148'
port = 8000
recv_size = 1024

ssh_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssh_client.connect((ipaddr, port))

while True:
    cmd = input(">>>: ")
    if cmd == "exit" or cmd == "quit": break 
    elif not cmd: continue    # 当用户输入为空的时候进入下一个循环
    ssh_client.send(cmd.encode('utf-8'))
    res_cmd = ssh_client.recv(recv_size)
    print(res_cmd.decode('utf-8'))
    
ssh_client.close()

11、粘包

在介绍什么时粘包之前我们先来了解一下TCP和UDP:

  • TCP:TCP是面向连接的 , 面向流的 , 提供高可靠性服务 . 收发两端都要一 一对应的socket。因此发送端为了更有效地将多个包发送到对端使用了一个优化算法(Nagle算法),该算法将多次发送的间隔小、数量小的数据包整合到一个大的数据块中进行封装。这时就需要提供一个合适的拆包机制才能合理分辨每一个数据包。
  • UDP:UDP 是无连接的 , 面向消息的 , 不使用块的合并优化算法的服务,由于UDP支持的是一对多的模式,所以在缓冲区采用了链式结构来记录每一个到达的UDP包,在每一个UDP包中都有消息头(消息来源地址和端口信息),这样就很容易进行区分处理了。
  • TCP和UDP的区别:TCP是基于数据流,于是在收发消息的时候不能为空,这就需要在客户端和服务端都添加相应的处理机制才能避免程序卡住或进入死循环;而UDP是基于数据报的,就算内容为空UDP也会自动加上消息头。

那什么是粘包?粘包从字面意思理解就是两个不同的数据包粘在了一起,就向如下的现象:

客户端

import  socket

ipaddr = '127.0.0.1'
port = 8000
back_log = 5
recv_size = 1024

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((ipaddr, port))
server.listen(back_log)


conn,addr = server.accept()
data1 = conn.recv(recv_size)
data2 = conn.recv(recv_size)
print("第一次:%s" %data1)
print("第二次:%s" %data2)

客户端

import socket

ipaddr = '127.0.0.1'
port = 8000
recv_size = 1024

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((ipaddr, port))


client.send("hello".encode('utf-8'))
client.send("word".encode('utf-8'))

客户端收到的结果:

第一次:b'helloword'
第二次:b''

这时第一种粘包情况:当发送端的数据小且间隔时间短时会造成两个包变为一个包

还要第二种粘包情况:当发生的数据过多时,接收端可能只接收到一部分内容,导致剩余的内容和下一个包的内容粘上;使用上面SSH的代码可以模拟该情况:

现象:

>>: ifconfig    
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.16.148  netmask 255.255.255.0  broadcast 192.168.16.255
        inet6 fe80::b61a:c99:edfb:528e  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:96:86:63  txqueuelen 1000  (Ethernet)
        RX packets 45906  bytes 11715999 (11.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 4821  bytes 2212944 (2.1 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 18548  bytes 7402001 (7.0 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 18548  bytes 7402001 (7.0 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

virbr0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 192.168.122.1  netmask 255.255.255.0  broadcast 192.168.12
>>: ls
2.255
        ether 52:54:00:07:02:e6  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0


>>: ls
socket_ssh_client.py
socket_ssh_server.py
tcp_server.py
upd_server.py

其实导致粘包的主要原因时因为接收方不知道消息之间的界限,不知道一次性需要提前多少字节。

粘包的解决方法:

我们既然知道了造成粘包的原因是接收方不知道消息之间的界限,那我们就给消息包前加上一个包含消息大小的消息头:

服务端

#!/usr/bin/python3
#-*- conding: utf-8-
# Filename: socket_ssh_server.py

import socket
import subprocess
import struct

ipaddr = '192.168.16.148'
port = 8000
back_log = 5
receive_size = 1024

ssh_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssh_server.bind((ipaddr, port))
ssh_server.listen(back_log)

while True:
    conn,addr = ssh_server.accept()
    while True:
        cmd = conn.recv(receive_size)
        if not cmd: break
        # print(cmd)
        res_cmd = subprocess.Popen(cmd, shell=True, 
                stdin = subprocess.PIPE, 
                stdout = subprocess.PIPE, 
                stderr = subprocess.PIPE)
        err_cmd = res_cmd.stderr.read()
        if err_cmd:
            conn.send("Command Error".encode("utf-8"))
            continue
        else:
            out_cmd = res_cmd.stdout.read()
        # 发送一个4个字节的并包含信息长度的报头
        conn.send(struct.pack('i', len(out_cmd)))
        
        conn.send(out_cmd)
    conn.close()

客户端

#!/usr/bin/python3
#-*- conding: utf-8-
# Filename: socket_ssh_client.py

import socket
import subprocess
import struct

ipaddr = '192.168.16.148'
port = 8000
receive_size = 1024

ssh_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssh_client.connect((ipaddr, port))

while True:
    cmd = input(">>: ")
    ssh_client.send(cmd.encode("utf-8"))
    # 接收信息长度
    res = ssh_client.recv(4)
    # 解包信息长度并获取
    length = struct.unpack('i', res)[0]

    data = ssh_client.recv(length)
    print(data.decode("utf-8"))

运行结果

>>: ls
socket_ssh_client.py
socket_ssh_server.py
tcp_server.py
upd_server.py

>>: ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.16.148  netmask 255.255.255.0  broadcast 192.168.16.255
        inet6 fe80::b61a:c99:edfb:528e  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:96:86:63  txqueuelen 1000  (Ethernet)
        RX packets 50285  bytes 12012009 (11.4 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5870  bytes 2513814 (2.3 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 20051  bytes 7920159 (7.5 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 20051  bytes 7920159 (7.5 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

virbr0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 192.168.122.1  netmask 255.255.255.0  broadcast 192.168.122.255
        ether 52:54:00:07:02:e6  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0


>>: ls
socket_ssh_client.py
socket_ssh_server.py
tcp_server.py
upd_server.py

12、使用socketserver模块实现socket并发

服务端

import socketserver
import subprocess
import struct

class Mysocket(socketserver.BaseRequestHandler):
    def handle(self):
        # 获取连接
        conn = self.request
        # 获取客户端地址
        add = self.client_address
        print(conn, add)
        # 进入收发循环
        while True:
            # 接收客户端发送的命令
            cmd = conn.recv(1024)
            if not cmd: continue
            print(cmd)
            # 执行客户端发送的命令
            res_cmd = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                       stdin=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
                                       stdout=subprocess.PIPE)
            err_cmd = res_cmd.stderr.read()
            out_cmd = res_cmd.stdout.read()
            if err_cmd:
                conn.sendall(struct.pack("i", len("命令错误!")))
                conn.sendall("命令错误!")
            else:
                conn.sendall(struct.pack("i", len(out_cmd)))
                conn.sendall(out_cmd)


if __name__ == '__main__':
    ipaddr = '127.0.0.1'
    port = 8000
    server = socketserver.ThreadingTCPServer((ipaddr, port), Mysocket)
    server.serve_forever()

客户端

import socket
import struct

ipaddr = '127.0.0.1'
port = 8000

client1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client1.connect((ipaddr, port))

while True:
    cmd = input(">>: ")
    client1.sendall(cmd.encode('utf-8'))
    res = client1.recv(4)
    # print(res)
    length = struct.unpack('i', res)[0]
    data = client1.recv(length)
    print(data.decode(''))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容