Python网络编程
网络基础
- 如何在网络中区分每一台计算机? 使用IP
- 一台计算机上多个程序如何共用网络而不冲突呢? 网络端口
- 不同的计算机通信怎么样才能互信理解? 使用相同的协议
#IP地址
- 用来在网络中标识一台计算机的一串数字,比如:192.168.34.203,在一段网络中是唯一的。
- 每一个IP地址包括两部分:网络地址和主机地址(分不同类)
- 主机号0,255两个数字是不能用的(网络号,广播地址)
#A\B\C\D\E类IP
- A类IP地址由1字节的⽹络地址和3字节主机地址组成, ⽹络地址的最⾼位必须是“0”,地址范围1.0.0.1-126.255.255.254可⽤的A类⽹络有126个, 每个⽹络能容纳1677214个主机
- B类IP地址由2个字节的⽹络地址和2个字节的主机地址组成, ⽹络地址的最⾼位必须是“10”,地址范围128.1.0.1-191.255.255.254 可⽤的B类⽹络有16384个,每个⽹络能容纳65534主机
- C类IP地址由3字节的⽹络地址和1字节的主机地址组成, ⽹络地址的最⾼位必须是“110”范围192.0.1.1-223.255.255.254 C类⽹络可达2097152个, 每个⽹络能容纳254个主机
- D类IP地址第⼀个字节以“1110”开始, 它是⼀个专⻔保留的地址。它并不指向特定的⽹络, ⽬前这⼀类地址被⽤在多点⼴播(一对多) 中多点⼴播地址⽤来⼀次寻址⼀组计算机 地址范围224.0.0.1-239.255.255.254
- E类IP地址以“1111”开始, 为将来使⽤保留 E类地址保留, 仅作实验和开发⽤
#私有IP
-
本地局域网上的IP,专门为组织机构内部使用
-
在这么多⽹络IP中, 国际规定有⼀部分IP地址是⽤于我们的局域⽹使⽤, 属于私⽹IP, 不在公⽹中使⽤的, 它们的范围是:
--10.0.0.0~10.255.255.255
--172.16.0.0~172.31.255.255
--192.168.0.0~192.168.255.255
-
私有IP:局域网通信,内部访问,不能在外网公用。私有IP禁止出现在Internet中,来自于私有IP的流量全部都会阻止并丢掉
-
公有IP:全球访问
-
IP地址127.0.0.1用于回路测试
- 测试当前计算机的网络通信协议
- 127.0.0.1可以代表本机IP地址, ⽤ http://127.0.0.1 就可以测试本机中配置的Web服务器
- 常用来ping 127.0.0.1来看本地ip/tcp正不正常,如能ping通即可正常使用
#子网掩码
-
是用于测试两个IP是不是属于同一网段的工具
-
子网掩码不能单独存在,它必须结合IP地址一起使用
-
⼦⽹掩码只有⼀个作⽤, 就是将某个IP地址划分成⽹络地址和主机地址两部分
-
⼦⽹掩码的设定必须遵循⼀定的规则:
--与IP地址相同, ⼦⽹掩码的长度也是32位
--左边是⽹络位, ⽤⼆进制数字“1”表示;
--右边是主机位, ⽤⼆进制数字“0”表示
-
假设IP地址为“192.168.1.1”⼦⽹掩码为“255.255.255.0”。
--其中,“1”有24个, 代表与此相对应的IP地址左边24位是⽹络号;
--“0”有8个代表与此相对应的IP地址右边8位是主机号
#端口号
-
用来标记区分进程
-
一台拥有IP地址的主机可以提供许多服务,比如HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)等,这些服务完全可以通过1个IP地址来实现。因为IP地址与网络服务是一对多的关系,实际上是通过IP地址和端口号相结合来区分不同的服务的。
-
端口号是一个数字,只有整数,范围是从0到65535(分知名和动态两种)
-
知名端口是装所周知的端口号(用来做固定的事情)
--80端口分配给了HTTP服务(网站)
--21端口分配了FTP服务(文件下载)
--可以理解为就是,一些常用的功能使用的端口号是固定的
-
动态端口是1024--65535
之所以是动态端口,是因为他一般不固定的分配某种服务,而是动态分配,动态分配是指当一个进程系统或者应用程序进行网络通信时,他向主机请求一个端口,主机从可用的端口中分配一个拱它使用。
#协议
-
协议:约定好的规范
-
早期的计算机网络,都是由各个厂商自己规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容(语言、方言、阿帕网)为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Interner Protocol Suite)就是通过协议标准
因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议总称为TCP/IP协议
-
TCP/IP协议是大家都遵循的最基本网络通信协议
-
是完成进程之间通信的规范
#不同层次
-
应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
-
但是一般写代码只需:应用层、传输层、网络层、网络接口层(数据链路层)四层即可。
#基础
在早期,不同的公司都推出了属于自己的私有网络协议,相互之间不能兼容于是,ISO(国际标准化组织)站出来:干脆这样,我给大家制定一个通用的网络通信协议,该协议是国际标准。 于是ISO博览众家之长,制订了“一堆”详细的,复杂的,繁琐的,精确的网络通信协议。• 不过这堆协议太复杂了,为了理清思路,便于学习,将他们分了7类(也就是分了7层),不同层代表不同的功能,并把这些协议归到相应的层里面去。• 国际标准出来了,接下来就要软件/硬件厂商去实现了。但实际上各厂商并没有完整实现7层协议,因为7层协议栈追求全能、完善,导致它太过复杂,实现起来太难了。
于是:实际使用的时候,就是按4层划分:应用层、传输层、网络层、网络接口层(链路层)
- TCP/IP定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准
- 4层的层级结构中,每一层都呼叫它的下一层所提供的网络来完成自己的需求
- 其中的应用层关注的是应用程序的细节,而不是数据在网络中的传输活动,其他三层主要处理所有的通信细节,对应用程序一无所知。
- 应用层
- 应用程序沟通的层,不同的文件系统有不同的文件命名原则和不同的文本行表示方法等,不同的系统之间传输文件还有各种不兼容问题,这些都将由应用层来处理
- 传输层
- 提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据协议(UDP)等,这一层负责传送数据,并且确定数据以被送达并被接收
- 网络层
- 负责提供基本的数据包传送功能,让每一块数据包都能够到达目的主机,网络层接收由更低层发来的数据包,并把该数据包发送到更高层,相反,IP层也把从TCP或UDP层接收来的数据包传送到更低层
- 网络接口层
- 对实际的网络媒体的管理,定义如何使用实际网络来传送数据(处理机械的、电气
的和过程的接口)
- 对实际的网络媒体的管理,定义如何使用实际网络来传送数据(处理机械的、电气
#Socket编程
- socket:通过网络完成进程间通信的过程(区别于一台计算机之间进程通信)
- socket英文翻译为“插孔”,也常被称为“套接字”
#Socket初识
- 本质是编程接口(API):Socket是对TCP/IP协议的封装,Socket只是个编程接口而不是协议,通过Socket我们才能使用TCP/IP协议簇
- TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或许显示数据的具体形式;Socket是发动机,提供了网络通信的能力
- 最重要的是:Socket是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的Socket系统调用
- 套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认
Socket编程
- 网络通信的方式一般情况只有两种:TCP和UDP
###创建Socket
from socket import * #导入套接字模块
s = socket(AF_INET,SOCK_STREAM) #创建套接字
s此时是一个socket对象,拥有发送和接收网络数据的功能
- 该函数带有的两个参数时必须要写的
- AF_INET(IPV4协议用于Ineternet进程间通信)
- SOCK_STREAM 套接字类型,可以是SOCK_STREAM(流式套接字,用于TCP协议)或者SOCK_DGRAM(数据报套接字,用于UDP协议)
- TCP慢但是稳定不会丢失
- UDP快但是可能会丢失数据(黑客攻击)
- 确定了IP地址端口号(ipv4协议),TCP或UDP协议之后,计算机之间可以进行通信
#Socket编程-UDP
- UDP --- User Data Protocol,用户数据报协议,是一个无连接的简单的面向数据报的传输层协议。UDP不提供可靠性,它只是把应⽤程序传给IP层的数据报发送出去, 但是并不能保证它们能到达⽬的地。由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接, 且没有超时重发等机制, 故⽽传输速度很快
- UDP⼀般⽤于多点通信和实时的数据业务, ⽐如:
- 语音广播
- 视频
- TFTP(简单文件传输)
#Socket编程-TCP
-
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接
-
一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,只简单
的描述下这三次对话的简单过程- 主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话
- 主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话
- 主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”
,这是第三次对话 - 三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据
#TCP与UDP之间的区别
- 基于连接与无连接
- 对系统资源的要求(TCP较多,UDP较少)
- UDP程序结构较简单
- 流模式与数据报模式
- TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证
- UDP理解为写信(只有收件人地址),TCP理解为打电话(先拨号建立通路,需要通路稳定)
#Socket-UDP编程
#UDP协议
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
#发送数据
from socket import *
Send_s1 = socket(AF_INET,SOCK_DGRAM) #创建套接字
Send_addr = ("192.168.34.131",54321) #准备接收方地址
Send_content = input("")
Send_s1.sendto(Send_content.encode("GB2312"),Send_addr)
###发送数据时,Python3需要将字符串转换成bytes类型
###encode("GB2312") 用GB2312进行编码,获取到Bytes类型对象
###decode()反过来
Send_s1.close()
#发送数据给飞秋
-
需要确定飞秋的端口号
-
发送普通数据,飞秋不会响应,必须发送特殊格式的内容
1:123123:李晨浩:李晨浩_PC:32:我是吴彦祖
因为飞秋有自己的应用层协议
1代表版本
1:后面的数字随便写,表示发送的时间
32代表发送消息的指令
-
使用while循环不延时发送可能会造成卡死
-
注意:P和端口在网络通信中缺一不可,用到的协议也要匹配,例如飞秋用的是udp协议,使用TCP协议发数据是无效的
from socket import *
s3 = socket(AF_INET,SOCK_DGRAM) #指定套接字
addr = ("192.168.34.45",2425) #绑定接收端的IP和端口
for i in range(101):
s3.sendto("1:123123:刘炳良:刘炳良_Com:32:你愁啥".encode("GB2312"),addr)
#接收数据
from socket import *
s = socket(AF_INET,SOCK_DGRAM) #创建套接字
s.bind(("",54323)) #绑定发送端的IP和端口 IP一般不用写
redata = s.recvfrom(1024) #1024表示接受的最大字节
print(redata[0].decode("GB2312")) #decode需要解码
s.close()
- 绑定信息:让一个进程可以使用固定的端口
- 一般情况下,发送端不绑定端口,接收端会绑定
#Echo服务器
- 用于调试和检测的工具,接收什么就原封发回什么
from socket import *
s1 = socket(AF_INET,SOCK_DGRAM)
addr = ("",54323)
s1.bind(addr)
num = 0
while 1:
data1 = s1.recvfrom(1024)
# print(data1)
num += 1
s1.sendto(data1[0],data1[1])
print(num,data1[0].decode("GB2312"))
#UDP网络通信过程
-
类似于发快递
1、应用层编写数据(你好),然后向下次传递
2、传输层在数据前面加上端口号(包括发送端口和目的端口)
3、网络层继续在前面加上IP地址(包括原IP和目的IP)
4、链路层再在前面加上Mac地址(硬件地址,用来定义网络设备的位置)
此时的数据变成了:mac地址 IP地址 端口号 数据内容
之后通过网络传输给另一台计算机的链路层开始逐步解析判断
#全双⼯的聊天程序
- 全双工(Full Duplex)是通讯传输的一个术语。通信允许数据在两个方向上同时传输(电话)
- 单工是只允许甲方向乙方传送信息,而乙方不能向甲方传送(收音机)
- 半双工:甲方发消息时乙方只能收不能发(对讲机)
import threading
from socket import *
from datetime import datetime
Now_time = datetime.strftime(datetime.now(),"%Y-%m-%d %H:%M:%S")
def send():
"""
发送端
:return:
"""
while 1:
Send_s1 = socket(AF_INET,SOCK_DGRAM)
Send_addr = ("192.168.34.131",54321)
Send_content = input("")
Send_s1.sendto(Send_content.encode("GB2312"),Send_addr)
def receive():
"""
接收端
:return:
"""
while 1:
Receive_s1 = socket(AF_INET,SOCK_DGRAM)
Receive_s1.bind(("",54322))
Receive_data = Receive_s1.recvfrom(2048)
print(f"【{Now_time}】:",Receive_data[0].decode("GB2312"))
th_send = threading.Thread(target=send)
th_receive = threading.Thread(target=receive)
th_send.start()
th_receive.start()
#编码
#UDP广播
- 当前网络上的所有电脑的某个进程都收到同一个数据(TCP没有广播)
from socket import *
import threading
def send_gb():
s1 = socket(AF_INET,SOCK_DGRAM)
#创建套接字
s1.setsockopt(SOL_SOCKET,SO_BROADCAST,1)
#设置套接字选项,以广播的形式发送数据
while 1:
addr = ("192.168.34.255", 6677)
#设置IP及端口
s1.sendto("这是爸爸的广播".encode(),addr)
def show_gb():
s = socket(AF_INET,SOCK_DGRAM)
s.bind(("",6677))
data = s.recvfrom(2048)
while 1:
print(data[0].decode())
send_t1 = threading.Thread(target=send_gb)
show_tw = threading.Thread(target=show_gb)
send_t1.start()
show_tw.start()
#Packet_Tracer
- Packet Tracer 是由Cisco(思科)公司发布的⼀个辅助学习⼯具,为学习思科⽹络课
程的初学者去设计、 配置、 排除⽹络故障提供了⽹络模拟环境(不用买硬件) - 可以提供数据包在网络中行进的详细处理过程,观察网络实时运行情况
(辅助学习网络通信过程)
#2台电脑相连
#集线器相连
#交换机相连
#路由器相连
#基本图形
#TFTP
- TFTP((Trivial File Transfer Protocol,简单⽂件传输协议),是TCP/IP协议簇中一个用来在客户端与服务器之间进行简单文件传输的协议
- 使用TFTP协议,就可以使用简单文件的下载
- 特点:
- 简单
- 占用资源小
- 适合传递小文件
- 适合在局域网进行传递
- 端口号为69
- 基于UDP实现
#Tftpd32
- Tftpd32:共享服务器(可以从本机共享文件)
#TFTP下载
- 有了服务器,还需要一个下载器
- 实现TFTP下载器:
- 下载:从服务器上将文件复制到本机上
- 下载的过程:
- 在本地创建一个空文件
- 向里面写数据(接收到一点就向空文件里写一点)
- 关闭(接受完所有的数据关闭文件)
#TFTP下载流程
#TFTP格式要求
![TFTP下载格式要求]](https://img2018.cnblogs.com/blog/1776444/201912/1776444-20191227131102935-105577159.png)
#struct模块
-
当客户端接收到数据小于516(2字节操作码+2字节块编号+512字节数据)时,就意味着服务器发送完毕了(如果恰好最后一个数据长度为516,会再发一个长度为0的数据包)
-
构造下载请求数据:“1test.jpg0octet0”
import struct cmb_buf = struct.pack(“!H8sb5sb”,1,b“test.jpg”,0,b“octet”,0) ##!H8sb5sb :!(感叹号)表示按照网络传输数据要求的形式来组织数据(占位的格式) H 表示将后面的1替换成两个字节 8s 相当于8个s(ssssssss)占8个字节,也就是对应test.jpg 占8个字节 b占一个字节
-
struct模块可以按照指定格式将Python数据转换为字符串,该字符串为字节流
-
struct模块中最重要的三个函数是pack(),unpack(),calcsize()
-
pack() 按照指定的格式(fmt),把数据封装成字符串(实际上类似c结构体的字节流)
pack(fmt, v1, v2, ...)
-
unpack() 按照给定的格式(fmt)解析字节流string,返回解析出来的元组unpack(fmt,string)
-
calcsize() 计算给定的格式(fmt)占用多少字节的内存
calcsize(fmt)
-
-
struct.pack(“!H8sb5sb",1,"test.jpg",0,"octet",0) --这是一个下载请求格式,发送的下载包
-
struct.pack("!HH",4,pnum) --这是一个确认包格式,发送的是确认包
-
Data_Bag = s.recvfrom(1024) --最大接受套接字,接收数据包 Opcode,num = struct.unpack("!HH",Data_Bag[0][:4]) --这是从数据包里获取操作码和块编号,Opcode代表操作码,num代表块编号
#C/Python对比
#从服务器下载文件
import struct
from socket import *
File_name = "qwe123.jpg"
#要下载的文件名
Dold_File = struct.pack("!H{}sb5sb".format(len(File_name.encode("GB2312"))),1,File_name.encode("GB2312"),0,"octet".encode("GB2312"),0)
#打包,构造下载请求数据,根据实际编码对编码格式进行更改
s = socket(AF_INET,SOCK_DGRAM)
#创建套接字
Addr = ("127.0.0.1",69)
# 服务器IP及地址
s.sendto(Dowlod_Re,Addr)
#第一次发送, 连接服务器69端口
file = open(File_name,"ab")
本地创建一个文件,可以与下载的文件名不一样,但是尽量一样,得是二进制
while 1:
Data_Bag = s.recvfrom(1024)
#s.recvfrom配置最大接受套接字,接收数据包
Opcode,num = struct.unpack("!HH",Data_Bag[0][:4])
#解包,从数据包里获取操作码和块编号,Opcode代表操作码,num代表块编号
print(Opcode,num)
if Opcode == 5:
#操作码为5代表有报错
print("Error")
break
Data = Data_Bag[0][4:]
#从数据包里拿出数据
file.write(Data)
#将数据写入到新的文件里
if len(Data) < 512:
#当数据字节小于512时,代表最后一个数据包里的数据不足512,表示传输完成
break
Ack = struct.pack("!HH",4,num)
#打包,重新构造下载请求数据,4是固定操作码,num代表从上一个数据包获取到的数据包的块编号
s.sendto(Ack,Data_Bag[1])
因为服务器第一次连接之后的端口号会随机改变,所以使用获取到的数据包里的IP和端口
#上传文件到服务器
from socket import *
import struct
s = socket(AF_INET,SOCK_DGRAM)
#创建套接字
File = "haotianqi.jpg"
#要上传的文件
File_name = struct.pack("!H{}sb5sb".format(len(File.encode("GB2312"))),2,File.encode("GB2312"),0,"octet".encode("GB2312"),0)
#构造上传请求数据,根据实际编码对编码格式进行更改
Addr = ("192.168.34.112",69)
#服务器的IP及端口
s.sendto(File_name,Addr)
#第一次发生,连接服务器及端口
file = open(File, "rb")
#以读字节的方式打开文件
while 1:
Data, Addr2 = s.recvfrom(1024)
#创建最大套接字,接收返回包里的返回数据和ip及端口,DATA代表数据,Addr2代表IP及端口
print(Data, Addr2)
#第一次连接时的端口是固定的,当开始传文件时,服务器会随机产生端口
Opcode, num = struct.unpack("!HH",Data[:4])
#解包,从数据包获取返回包的操作码和块编号
print(Opcode, num)
if Opcode == 4:
#获取到的操作码是4说明返回确认包ACK
file_data = file.read(512)
#需要上传的文件,上传数据最大为512字节
File_name2 = struct.pack("!HH",3,num+1) + file_data
#构造上传数据,3是固定操作码,num是上一次向服务器写的块编号,再次上传的时候需要将块编号进行+1
s.sendto(File_name2,Addr2)
#发送上传的数据
if len(file_data) < 512:
#如果发送的数据字节小于512,表示上传完毕
print("上传完毕")
elif Opcode == 5:
#如果操作码等于5,表示有错误产生
print("Error")
#Socket-TCP编程
#TCP
- 传输控制协议(使用情况多余udp)
- 稳定:保障数据一定能收到
- 相对UDP会慢一点
- web服务器一般都是使用TCP(银行转账,稳定比快要重要)
#TCP协议及通信模型
-
协议:TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
-
在通信之前,必须先等待建立链接
#TCP三次握手
- 第一次握手:建立连接时,客户端发送SYN(请求同步)包(SYN=x)到服务器,并进入SYN_SENT(请求连接)状态,等待服务器确认
- 第二次握手:服务器收到SYN包,必须确认客户的SYN,然后在客户的SYN值上+1,得到一个ACK的包,同时自己也发送一个SYN包(SYN=y),即SYN+ACK包,此时服务器进入SYN_RECV(SYN派遣)状态
- 第三次握手:客户端收到服务器的SYN+ACK包,然后在服务器的SYN包的值上+1,得到一个ACK的包,并向服务器将ACK确认包发送过去,此包发送完毕后,客户端和服务端进入ESTABLISHED(TCP连接成功)状态,完成三次握手,客户端与服务器才正式开始传送数据。
理想状态下,TCP连接一旦建立,在通信双方的任何一方主动关闭连接之前,TCP连接都将被一直保持下去。
![三次握手]](https://img2018.cnblogs.com/blog/1776444/201912/1776444-20191227131310533-2037806882.png)
- 在TCP传输过程中,如果有一方如果收到对方的数据,一定会发生一个ACK确认包给发送方
#TCP四次挥手
-
第一次挥手:主动关闭方调用close,会发送一个长度为0的数据包以及FIN(结束标志),用来关闭 主动关闭方 到 被动关闭方 之间的数据传送
-
第二次挥手:被动关闭方收到FIN包之后,会发送一个ACK给对方,确认序号为收到的序号+1
-
第三次挥手:被动关闭方发送一个FIN后,用来关闭 被动关闭方 到 主动关闭方之间的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
-
第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到的序号+1,到此时,完成四次挥手
#TCP服务器
- 长连接:三次握手四次挥手之间分多次传递完所有数据(优酷看视频、在线游戏),
长时间占用某个套接字 - 短连接:三次握手四次挥手之间传递少部分数据,多次握手挥手才传递完所有数据
(浏览器),短时间占用
#TCP服务器流程
- socket绑定一个套接字
- bind绑定IP和端口
- listen设置最大连接数,收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求
- accept等待客户端的链接及连接和接收请求
- recv收数据、send发数据
#TCP单进程服务器
from socket import *
one_socket = socket(AF_INET,SOCK_STREAM)#创建一个套接字
addr = ("192.168.34.131",54322) #IP和端口
one_socket.bind(addr) #给套接字绑定IP和端口
one_socket.listen(5) #设置最大排队连接
while 1:
new_tockte,new_addr = one_sockte.accept()
#new_tockte为客户端返回的新的套接字,每当一个客户连接时,就会产生一个新的套接字,因为原套接字需要等待其余用户的连接
new_tocket.send("收到".encode())
#给客户端发送消息
data = new_tocket.recv(1024)
print(data.decode())
new_tocket.close()
#TCP客户端
from socket import *
s = socket(AF_INET,SOCK_STREAM) #创建套接字
addr = ("192.168.34.131",54322) #设定服务器IP和端口
s.connect(addr) #连接服务器
while 1:
name = input(":")
if name == "886":
break
s.send(name.encode())
data = s.recv(1024)
print(data.decode())
#TCP多进程服务器
- one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
- 重设套接字,重复使用绑定的信息
- 当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到SO_REUSEADDR选项。
from socket import *
from multiprocessing import *
num = 0
def sild_child(two_socket,two_addr):
try:
while 1:
data = two_socket.recv(1024) #接收新的套接字返回的数据
#当传输的数据为0字节的时候,服务器会一直在等待套接字返回的数据。直到客户端进程退出
if len(data) > 0:
print(f"IP:{two_addr[0]},PORT:{two_addr[1]}:{data.decode()}")
two_socket.send("爸爸收到了".encode())
else:
print(f"IP:{two_addr[1]}已经关闭")
break
except Exception as e:
print("ERROR")
finally:
two_socket.close() #将该套接字关闭
def main():
one_socket = socket(AF_INET,SOCK_STREAM)#创建套接字
one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #重设套接字,重复使用绑定的信息
addr = ("192.168.34.131",54322)
one_socket.bind(addr)#给套接字绑定地址
one_socket.listen(5)#设置最大排练连接
try:
while 1:
two_socket,two_addr = one_socket.accept()#当有客户访问时,会产生新的套接字,三次握手也在此解决
global num
num += 1
print(f"进程{num}---IP:{two_addr[0]}已连接")
child = Process(target=sild_child,args=(two_socket,two_addr))
child.start()
two_socket.close() #将该套接字关闭
except Exception as e:
print("ERROR")
finally:
one_socket.close() #关闭套接字
if __name__ == "__main__":
main()
#TCP多线程服务器
- 耗费资源比多进程小
from socket import *
import threading
def sild_child(two_socket,two_addr):
try:
while 1:
data = two_socket.recv(1024)
if len(data) > 0:
print(f"IP:{two_addr[0]},PORT:{two_addr[1]}:{data.decode()}")
two_socket.send("爸爸收到了".encode())
else:
print(f"IP:{two_addr[1]}已经关闭")
break
except Exception as e:
print("ERROR")
finally:
two_socket.close()
def main():
one_socket = socket(AF_INET,SOCK_STREAM)
one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
addr = ("192.168.34.131",54323)
one_socket.bind(addr)
one_socket.listen(5)
try:
while 1:
two_socket,two_addr = one_socket.accept()
print("新的套接字已返回")
print(f"IP:{two_addr[0]}已连接")
child = threading.Thread(target=sild_child,args=(two_socket,two_addr))
child.start()
#用进程的话,此处的套接字需要close关闭,但是此处是线程,不用关闭
#因为线程中共享这个套接字, 如果关闭了会导致这个套接字不可⽤
#但是此时在线程中这个套接字可能还在收数据, 因此不能关闭
except Exception as e:
print("ERROR")
finally:
one_socket.close()
if __name__ == "__main__":
main()
#Socketserver
#进阶版TCP并发服务器
- 可以使用socketserver来创建socket用来简化并发服务器
- socketserver可以实现和多个客户端通信(实现并发处理多个客户端请求的Socket服务端),他是在socket的基础上进行了一层封装,也就是说底层还是调用socket
- 服务器接收客户端连接请求>实例化一个请求处理程序>根据服务器类和请求处理程序类,调用处理方法。
- 例如:基本请求程序类(BaseRequesHandler)调用方法handle。此方法通过属性self.request来访问客户端套接字
#创建服务端
import socketserver
class Myserver(socketserver.BaseRequestHandler):
#创建了一个请求处理的类,必须继承BaseRequestHandler,来实现通信循环,并且得重写handle方法(),
def handle(self):
#在handle()中处理和客户端所有的交互,当建立连接后会自动执行handle()方法
while 1:
one_data = self.request.recv(1024)
#self.request相当于客户端返回的套接字
#handle通过属性self.request来访问客户端套接字,
print(one_data.decode())
self.request.send("爸爸收到了".encode())
socketserver.TCPServer.allow_reuse_address = True
允许地址端口重用
addr = ("192.168.34.131",54325)
server = socketserver.ThreadingTCPServer(addr,Myserver)
#实例化对象,将IP和端口,以及自己定义的类传入,
server.serve_forever()
#对象执行server.forever()方法后,开启服务端,
#server.forever()能够处理多个请求
#handle_request()只处理一个请求
#客户端
from socket import *
import time
s = socket(AF_INET,SOCK_STREAM)
addr = ("192.168.34.131",54325)
s.connect(addr)
while 1:
name = input(":")
if name == "886":
break
s.send(name.encode())
data = s.recv(1024)
print(data.decode())
#subprocess
#subprocess
- Python可以使用subprocess下的Popen类中的封装的方法来执行命令
- 构造方法 popen() 创建popen类的实例化对象
obj = Subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
data 表示命令内容
shell = Ture 表示命令解释器,相当于在终端执行命令
stdout=subprocess.PIPE 表示正确的内容
stderr=subprocess.PIPE 表示错误的内容
PIPE是将结构转移到当前进程
- 通过stdout.read()获取命令执行的结果
- 通过stderr.read()获取报错的内容
- 指定结果后会将执行结果封装到指定的对象中
- 然后通过对象.stdout.read()获取执行命令的结果,如果不定义stdout会将结果进行标准输出
#subprocess格式
import subprocess
obj = subprocess.Popen(
"dir", #命令内容
shell=True, #命令解释器,就是终端窗口
stdout = subprocess.PIPE, #正确结果
stderr = subprocess.PIPE #错误结果
)
cor = obj.stdout.read().decode("gbk") #获得正确结果的内容
err = obj.stdout.read().decode("gbk") #获得错误结果的内容
print(cor + err)
#实现远程执行服务器
from socket import *
from multiprocessing import *
import struct
import subprocess
def sild_child(two_socket,two_addr):
try:
while 1:
data = two_socket.recv(1024)
if len(data) > 0:
print(f"{two_addr[0]}-命令为:{data.decode()}")
obj = subprocess.Popen(
data.decode("gbk"),
shell = True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
cor = obj.stdout.read()
err = obj.stderr.read()
allrr = cor+err
two_socket.send(allrr) #发送数据
else:
print(f"IP:{two_addr[1]}已经关闭")
break
except ConnectionResetError:
print("服务异常")
two_socket.close()
def main():
one_socket = socket(AF_INET,SOCK_STREAM)
one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
addr = ("192.168.34.131",54323)
one_socket.bind(addr)
one_socket.listen(5)
num = 0
try:
while 1:
num += 1
two_socket,two_addr = one_socket.accept()
print(f"进程{num}---IP:{two_addr[0]}已连接")
child = Process(target=sild_child,args=(two_socket,two_addr))
child.start()
two_socket.close()
except Exception as e:
print("连接异常")
one_socket.close()
if __name__ == "__main__":
main()
#客户端
from socket import *
import struct
client = socket(AF_INET,SOCK_STREAM)
addr = ("192.168.34.131",54323)
client.connect(addr)
while 1:
name = input(":")
if name == "886":
break
client.send(name.encode())
data = client.recv(1024)
print(data.decode("gbk"))
#沾包
#TCP和UDP协议
- TCP:TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP:UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。
#沾包现象
只有TCP会出现沾包现象,UDP永远不会出现沾包
应用程序看到的数据是一个整体,或者说是一个流,一条消息有多少字节对应程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现沾包的原因
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位对数据进行提取,不能一次提取任意字节的数据,这一点和TCP有很大的不同。那一条消息又该如何定义?
消息可以认为是为对方一次性write/send的数据为一个消息,需要知道的是当对方send一条消息的时候,无论底层怎么进行分段分片,TCP协议会把整条消息的数据段排序完成后才呈现在内核缓冲区
当TCP套接字客户端往服务端上传文件时,发送文件内容都是按照一段一段的字节流进行发送的,接收方看了,根本不知道文件的字节流从何处开始,在何处结束。所谓的沾包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
__沾包不一定会发生,但是发生了,就会有两种情况:__71、在客户端发生了沾包
-
发送端需要等待缓冲区满了才发送出去,造成沾包(发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包)
-
2、在服务端发生了沾包
接收方不及时接受缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
#沾包解决
#第一种:
-
send的时候加上sleep
虽然可以解决,但是会影响效率,不可取
#第二种
问题的根源在与:接收端不知道发送端要传送的字节流的长度
- 所以解决沾包的方法就是在发送端在发送数据之前,发送一个头文件,告诉发送的字节流总大小,然后接收端来一个死循环接收完所以数据
- 使用struck模块可以将Python的值根据格式符,转换为固定长度的字符串(bytes类型)
####服务端###
from socket import *
import threading
import struct
import subprocess
def sild_child(two_socket,two_addr):
try:
while 1:
data = two_socket.recv(1024)
if len(data) > 0:
print(f"{two_addr[0]}-命令为:{data.decode()}")
#远程操作服务器
obj = subprocess.Popen(
data.decode("GBK"),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
out = obj.stdout.read()
err = obj.stderr.read()
allread = out + err
###沾包处理
allsize = len(allread) #获取包数据的总长度
headsize = struct.pack("i",allsize) #制作数据包头
two_socket.send(headsize) ##将固定长度的包头发送
two_socket.send(allread) #将数据发送
except ConnectionResetError:
print("服务异常")
finally:
two_socket.cloes()
def main():
one_socket = socket(AF_INET,SOCK_STREAM)
one_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
addr = ("192.168.34.131",54325)
one_socket.bind(addr)
one_socket.listen(5)
num = 0
try:
while 1:
num += 1
two_socket,two_addr = one_socket.accept()
print(f"进程{num}---IP:{two_addr[0]}已连接")
child = threading.Thread(target=sild_child,args=(two_socket,two_addr))
child.start()
except Exception as e:
print("Connection exception")
one_socket.close()
if __name__ == "__main__":
main()
####客户端
from socket import *
import struct
sliend = socket(AF_INET,SOCK_STREAM)
addr = ("192.168.34.131",54325)
sliend.connect(addr)
while 1:
senddata = input(":")
if senddata == "886":
break
sliend.send(senddata.encode())
headsize = sliend.recv(4) #接受服务端发送的包头(只接受4字节)
allsize = struct.unpack("i",headsize)[0] #获取总数据字节大小
data = b""
while len(data) < allsize:
data += sliend.recv(1024)
print(data.decode("GBK"))