天天看点

[017]Python网络编程_基础_全栈基础

您好!此笔记的文本和代码以网盘形式分享于文末!

因个人能力有限,错误处欢迎大家交流和指正!基础部分内容简单,但多且零散!

# 网络编程

"""

网络基础补充:

”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络

端口:是设备与外界通讯交流的出口,通过“IP地址+端口号”来区分不同的服务的。

ip地址精确到具体的一台电脑,而端口精确到具体的程序。

TCP协议:TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。

建立连接TCP三次握手的过程如下:

客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。

服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。

客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。

三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了

终止连接:四次握手,这是由TCP的半关闭(half-close)造成的

1、该端的TCP于是发送一个FIN分节,表示数据发送完毕。

2、接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

3、一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。

这导致它的TCP也发送一个FIN。

4、 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

UDP协议:UDP与一个应用程序通信时,传输数据之前源端和终端不建立连接

TCP和UDP的比较:

TCP---传输控制协议,提供的是面向连接、可靠的字节流服务,

客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,

之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,

流量控制等功能,保证数据能从一端传到另一端。

UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。

UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,

但是并不能保证它们能到达目的地。

由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,

且没有超时重发等机制,故而传输速度很快

互联网协议与OSI模型

一、应用层

    1、应用层

        1>应用层

        2>表示层

        3>会话层

二、传输层

    1、传输层

        1>传输层

            物理设备:四层交换机、四层路由器

            常见协议:TCP与UDP协议

三、网络层

    1、网络层

        1>网络层

            物理设备:路由器、三层交换机

            常见协议:IP协议

四、网络接口层

    1、数据链路层

        1>数据链路层

            物理设备:网桥、以太网、网卡

            常见协议:arp协议--地址解析协议

    2、物理层

        1> 物理层

            物理设备:中继器、集线器、双绞线

软件开发架构

1.应用类C/S架构:客户端与服务器端架构

2.web类B/S架构:浏览器端与服务器端架构

socket层:是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,

Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面。

(用户进程)应用层 -- socket抽象层-- (TCP/UDP)传输层 -- (ICMP/IP)网络层

套接字(socket)

TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、

传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。

使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小)

一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。

使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

TCP server端:

import socket

sk = socket.socket()

# # socket配置,重用ip和端口

# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)

# 把地址绑定到套接字

sk.bind(('127.0.0.1',8898))

# 监听链接

sk.listen()

# 接受客户端链接

conn,addr = sk.accept()

# 接收客户端信息

ret = conn.recv(1024)

# 打印客户端信息

print(ret)

# 向客户端发送信息

conn.send(b'hi')

# 关闭客户端套接字

conn.close()

# 关闭服务器套接字(可选)

sk.close()

TCP client端:

import socket

# 创建客户套接字

sk = socket.socket()

# 尝试连接服务器

sk.connect(('127.0.0.1',8898))

sk.send(b'hello!')

# 对话(发送/接收)

ret = sk.recv(1024)

print(ret)

# 关闭客户套接字

sk.close()

基于UDP协议的socket

UDP server端:

import socket

# 创建一个服务器的套接字

udp_sk = socket.socket(type=socket.SOCK_DGRAM)

# 绑定服务器套接字

udp_sk.bind(('127.0.0.1',9000))

msg,addr = udp_sk.recvfrom(1024)

print(msg)

# 对话(接收与发送)

udp_sk.sendto(b'hi',addr)

# 关闭服务器套接字

udp_sk.close()

UDP client端:

import socket

ip_port=('127.0.0.1',9000)

udp_sk=socket.socket(type=socket.SOCK_DGRAM)

udp_sk.sendto(b'hello',ip_port)

back_msg,addr=udp_sk.recvfrom(1024)

print(back_msg.decode('utf-8'),addr)

"""

"""

socket参数的详解:

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

family:地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。

       (AF_UNIX 域实际上是使用本地 socket 文件来通信)

type: 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一

      SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)

      面向连接的SOCKET,多用于资料传送.

      SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息.

proto: 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,

        协议应为CAN_RAW或CAN_BCM之一。

fileno:如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。

       与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。

       这可能有助于使用socket.close()关闭一个独立的插座。

"""

"""

黏包:同时执行多条命令之后,得到的结果很可能只有一部分,

在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。

TCP 实现黏包:

server端:

from socket import *

import subprocess

ip_port=('127.0.0.1',8888)

BUFSIZE=1024

tcp_socket_server=socket(AF_INET,SOCK_STREAM)

tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)

tcp_socket_server.bind(ip_port)

tcp_socket_server.listen(5)

while True:

    conn,addr=tcp_socket_server.accept()

    print('客户端',addr)

    while True:

        cmd=conn.recv(BUFSIZE)

        if len(cmd) == 0:break

        res=subprocess.Popen(cmd.decode('utf-8'),shell=True,

                         stdout=subprocess.PIPE,

                         stdin=subprocess.PIPE,

                         stderr=subprocess.PIPE)

        stderr=res.stderr.read()

        stdout=res.stdout.read()

        conn.send(stderr)

        conn.send(stdout)

client端:

import socket

BUFSIZE=1024

ip_port=('127.0.0.1',8888)

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

res=s.connect_ex(ip_port)

while True:

    msg=input('>>: ').strip()

    if len(msg) == 0:continue

    if msg == 'quit':break

    s.send(msg.encode('utf-8'))

    act_res=s.recv(BUFSIZE)

    print(act_res.decode('utf-8'),end='')

基于UDP实现黏包:

server端:

from socket import *

import subprocess

ip_port=('127.0.0.1',9000)

bufsize=1024

udp_server=socket(AF_INET,SOCK_DGRAM)

udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)

udp_server.bind(ip_port)

while True:

    #收消息

    cmd,addr=udp_server.recvfrom(bufsize)

    print('用户命令----->',cmd)

    #逻辑处理

    res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE)

    stderr=res.stderr.read()

    stdout=res.stdout.read()

    #发消息

    udp_server.sendto(stderr,addr)

    udp_server.sendto(stdout,addr)

udp_server.close()

client端:

from socket import *

ip_port=('127.0.0.1',9000)

bufsize=1024

udp_client=socket(AF_INET,SOCK_DGRAM)

while True:

    msg=input('>>: ').strip()

    udp_client.sendto(msg.encode('utf-8'),ip_port)

    err,addr=udp_client.recvfrom(bufsize)

    out,addr=udp_client.recvfrom(bufsize)

    if err:

        print('error : %s'%err.decode('utf-8'),end='')

    if out:

        print(out.decode('utf-8'), end='')

****** 只有TCP有粘包现象,UDP永远不会粘包

黏包的成因:

1、TCP的拆包机制:

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

MTU:网络上传送的最大数据包。MTU的单位是字节,大部分网络设备的MTU都是1500,会产生

很多数据包碎片,增加丢包率,降低网络速度

2、面向流的通信特点和Nagle算法:

TCP-是面向连接的,面向流的,提供高可靠性服务。收发两端都要有一一成对的socket,

发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),

将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包.tcp是基于数据流的,

于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住

可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,

己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包

黏包现象只发生在tcp协议中:

1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。

2.实际上,主要还是因为接收方不知道消息之间的界限,

不知道一次性提取多少字节的数据所造成的

解决黏包一:

如何让发送端在发送数据前,

把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

server端:

import socket,subprocess

ip_port=('127.0.0.1',8080)

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

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(ip_port)

s.listen(5)

while True:

    conn,addr=s.accept()

    print('客户端',addr)

    while True:

        msg=conn.recv(1024)

        if not msg:break

        res=subprocess.Popen(msg.decode('utf-8'),shell=True,\

                            stdin=subprocess.PIPE,\

                         stderr=subprocess.PIPE,\

                         stdout=subprocess.PIPE)

        err=res.stderr.read()

        if err:

            ret=err

        else:

            ret=res.stdout.read()

        data_length=len(ret)

        conn.send(str(data_length).encode('utf-8'))

        data=conn.recv(1024).decode('utf-8')

        if data == 'recv_ready':

            conn.sendall(ret)

    conn.close()

client端:

import socket,time

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

res=s.connect_ex(('127.0.0.1',8080))

while True:

    msg=input('>>: ').strip()

    if len(msg) == 0:continue

    if msg == 'quit':break

    s.send(msg.encode('utf-8'))

    length=int(s.recv(1024).decode('utf-8'))

    s.send('recv_ready'.encode('utf-8'))

    send_size=0

    recv_size=0

    data=b''

    while recv_size < length:

        data+=s.recv(1024)

        recv_size+=len(data)

    print(data.decode('utf-8'))

上面的方式,放大网络延迟带来的性能损耗,会带来性能损耗。

struct模块的方式解决:

模块可以把要发送的数据长度转换成固定长度的字节

import json,struct

#假设通过客户端上传1T:1073741824000的文件a.txt

#为避免粘包,必须自定制报头

header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值

#为了该报头能传送,需要序列化并且转为bytes

head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输

#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节

head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度

#客户端开始发送

conn.send(head_len_bytes) #先发报头的长度,4个bytes

conn.send(head_bytes) #再发报头的字节格式

conn.sendall(文件内容) #然后发真实内容的字节格式

#服务端开始接收

head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式

x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度

head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式

header=json.loads(json.dumps(header)) #提取报头

#最后根据报头的内容提取真实的数据,比如

real_data_len=s.recv(header['file_size'])

s.recv(real_data_len)

struct的详细用法:

__author__ = 'Linhaifeng'

import struct

import binascii

import ctypes

values1 = (1, 'abc'.encode('utf-8'), 2.7)

values2 = ('defg'.encode('utf-8'),101)

s1 = struct.Struct('I3sf')

s2 = struct.Struct('4sI')

print(s1.size,s2.size)

prebuffer=ctypes.create_string_buffer(s1.size+s2.size)

print('Before : ',binascii.hexlify(prebuffer))

# t=binascii.hexlify('asdfaf'.encode('utf-8'))

# print(t)

s1.pack_into(prebuffer,0,*values1)

s2.pack_into(prebuffer,s1.size,*values2)

print('After pack',binascii.hexlify(prebuffer))

print(s1.unpack_from(prebuffer,0))

print(s2.unpack_from(prebuffer,s1.size))

s3=struct.Struct('ii')

s3.pack_into(prebuffer,0,123,123)

print('After pack',binascii.hexlify(prebuffer))

print(s3.unpack_from(prebuffer,0))

使用struct解决黏包 :

发送时:

    先发送struct转换好的数据长度4字节,再发送数据。

接收时:

    先接受4个字节使用struct转换成数字来获取要接收的数据长度,再按照长度接收数据。

server端:

import socket,struct,json

import subprocess

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

phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

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

phone.listen(5)

while True:

    conn,addr=phone.accept()

    while True:

        cmd=conn.recv(1024)

        if not cmd:break

        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),

                             shell=True,

                             stdout=subprocess.PIPE,

                             stderr=subprocess.PIPE)

        err=res.stderr.read()

        print(err)

        if err:

            back_msg=err

        else:

            back_msg=res.stdout.read()

        conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度

        conn.sendall(back_msg) #在发真实的内容

    conn.close()

client端:

import socket,time,struct

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

res=s.connect_ex(('127.0.0.1',8080))

while True:

    msg=input('>>: ').strip()

    if len(msg) == 0:continue

    if msg == 'quit':break

    s.send(msg.encode('utf-8'))

    l=s.recv(4)

    x=struct.unpack('i',l)[0]

    print(type(x),x)

    # print(struct.unpack('I',l))

    r_s=0

    data=b''

    while r_s < x:

        r_d=s.recv(1024)

        data+=r_d

        r_s+=len(r_d)

    # print(data.decode('utf-8'))

    print(data.decode('gbk')) #windows默认gbk编码

还可以将报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,

然后用struck将序列化后的数据长度打包成4个字节

发送时:

    先发报头长度

    再编码报头内容然后发送

    最后发真实内容

接收时:

    先收报头长度,用struct取出来

    根据取出的长度收取报头内容,然后解码,反序列化

    从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

服务端:

import socket,struct,json

import subprocess

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

phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

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

phone.listen(5)

while True:

    conn,addr=phone.accept()

    while True:

        cmd=conn.recv(1024)

        if not cmd:break

        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),

                             shell=True,

                             stdout=subprocess.PIPE,

                             stderr=subprocess.PIPE)

        err=res.stderr.read()

        print(err)

        if err:

            back_msg=err

        else:

            back_msg=res.stdout.read()

        headers={'data_size':len(back_msg)}

        head_json=json.dumps(headers)

        head_json_bytes=bytes(head_json,encoding='utf-8')

        conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度

        conn.send(head_json_bytes) #再发报头

        conn.sendall(back_msg) #在发真实的内容

    conn.close()

客户端:

from socket import *

import struct,json

ip_port=('127.0.0.1',8080)

client=socket(AF_INET,SOCK_STREAM)

client.connect(ip_port)

while True:

    cmd=input('>>: ')

    if not cmd:continue

    client.send(bytes(cmd,encoding='utf-8'))

    head=client.recv(4)

    head_json_len=struct.unpack('i',head)[0]

    head_json=json.loads(client.recv(head_json_len).decode('utf-8'))

    data_len=head_json['data_size']

    recv_size=0

    recv_data=b''

    while recv_size < data_len:

        recv_data+=client.recv(1024)

        recv_size+=len(recv_data)

    print(recv_data.decode('utf-8'))

    #print(recv_data.decode('gbk')) #windows默认gbk编码

**** FTP作业: 上传下载文件

socket的更多方法:

服务端套接字函数

s.bind()    绑定(主机,端口号)到套接字

s.listen()  开始TCP监听

s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数

s.connect()     主动初始化TCP服务器连接

s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数

s.recv()            接收TCP数据

s.send()            发送TCP数据

s.sendall()         发送TCP数据

s.recvfrom()        接收UDP数据

s.sendto()          发送UDP数据

s.getpeername()     连接到当前套接字的远端的地址

s.getsockname()     当前套接字的地址

s.getsockopt()      返回指定套接字的参数

s.setsockopt()      设置指定套接字的参数

s.close()           关闭套接字

面向锁的套接字方法

s.setblocking()     设置套接字的阻塞与非阻塞模式

s.settimeout()      设置阻塞套接字操作的超时时间

s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数

s.fileno()          套接字的文件描述符

s.makefile()        创建一个与该套接字相关的文件

"""

# 待补充:

# 验证客户端链接的合法性

# socketserver

愿有更多的朋友,在网页笔记结构上分享更逻辑和易读的形式:

链接:暂无

提取码:暂无

继续阅读