TCP與UDP協議
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
- tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略
粘包現象
socket收發消息的原理

應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。
而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。怎樣定義消息呢?
可以認為對方一次性write/send的數據為一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成后才呈現在內核緩沖區。
#1:不管是recv還是send都不是直接接收對方的數據,而是操作自己的操作系統內存--->不是一個send對應一個recv
#2:recv:
wait data 耗時非常長
copy data
send:
copy data
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
只有TCP有粘包現象,UDP永遠不會粘包
粘包不一定會發生:
如果發生了:1.可能是在客戶端已經粘了
2.客戶端沒有粘,可能是在服務端粘了
客戶端粘包:
發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據量很小,TCP優化算法會當做一個包發出去,產生粘包)
client端:
import socket
import time
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',9904))
client.send('hello'.encode('utf-8'))
client.send('world'.encode('utf-8'))
server端:
import socket
import time
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',9904)) #0-65535:0-1024給操作系統使用
server.listen(5)
conn, addr=server.accept()
print('connect by ',addr)
res1 = conn.recv(100)
print('第一次',res1)
res2=conn.recv(10)
print('第二次', res2)
服務端輸出結果
connect by ('127.0.0.1', 9787)
第一次 b'helloworld'
第二次 b''
服務端粘包
接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
server端:
import socket
import time
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',9904)) #0-65535:0-1024給操作系統使用
server.listen(5)
conn, addr=server.accept()
print('connect by ',addr)
res1 = conn.recv(2)#第一沒有接收完整
print('第一次',res1)
time.sleep(6)
res2=conn.recv(10)# 第二次會接收舊數據,再收取新的
print('第二次', res2)
client端
import socket
import time
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',9904))
client.send('hello'.encode('utf-8'))
time.sleep(5)
client.send('world'.encode('utf-8'))
服務端輸出
connect by ('127.0.0.1', 10184)
第一次 b'he'
第二次 b'lloworld'
解決粘包問題
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是發送端在發送數據前,發一個頭文件包,告訴發送的字節流總大小,然后接收端來一個死循環接收完所有數據
使用struct模塊可以用於將Python的值根據格式符,轉換為字符串(byte類型)
struct模塊中最重要的三個函數是pack(), unpack(), calcsize()
pack(fmt, v1, v2, ...) 按照給定的格式(fmt),把數據封裝成字符串(實際上是類似於c結構體的字節流)
unpack(fmt, string) 按照給定的格式(fmt)解析字節流string,返回解析出來的tuple
calcsize(fmt) 計算給定的格式(fmt)占用多少字節的內存
struct中支持的格式如下表:
| Format | C Type | Python | 字節數 |
|---|---|---|---|
| x | pad byte | no value | 1 |
| c | char | string of length 1 | 1 |
| b | signed char | integer | 1 |
| B | unsigned char | integer | 1 |
| ? | _Bool | bool | 1 |
| h | short | integer | 2 |
| H | unsigned short | integer | 2 |
| i | int | integer | 4 |
| I | unsigned int | integer or long | 4 |
| l | long | integer | 4 |
| L | unsigned long | long | 4 |
| q | long long | long | 8 |
| Q | unsigned long long | long | 8 |
| f | float | float | 4 |
| d | double | float | 8 |
| s | char[] | string | 1 |
使用案例
import struct
res = struct.pack('i',123)
print(res,type(res), len(res)) # b'{\x00\x00\x00' <class 'bytes'> 4 封裝一個4個字節的包
res1=struct.pack('q',11122232323)
print(res1,type(res1), len(res1)) # b'\x03\xcc\xef\x96\x02\x00\x00\x00' <class 'bytes'> 8 封裝一個8個字節的包
print(struct.unpack('i',res)[0]) # 拆包
print(struct.unpack('q',res1)[0]) #
#輸出
# b'{\x00\x00\x00' <class 'bytes'> 4
# b'\x03\xcc\xef\x96\x02\x00\x00\x00' <class 'bytes'> 8
# (123,)
# (11122232323,)
解決粘包問題簡單版(適用於傳輸字節較小)
server
import socket
import subprocess
import struct
def cmd_exec(cmd):
"""
執行shell命令
:param cmd:
:return:
"""
p = subprocess.Popen(cmd, shell=True,
stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
return stderr
return stdout
sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用地址端口
sock_server.bind(('127.0.0.1', 8088))
sock_server.listen(1) # 開始監聽,1代表在允許有一個連接排隊,更多的新連接連進來時就會被拒絕
print('starting...')
while True:
conn, client_addr = sock_server.accept() # 阻塞直到有連接為止,有了一個新連接進來后,就會為這個請求生成一個連接對象
print(client_addr)
while True:
try:
data = conn.recv(1024) # 接收1024個字節
if not data: break # 適用於linux操作系統,防止客戶端斷開連接后死循環
print('客戶端的命令', data.decode('gbk'))
res = cmd_exec(data.decode('gbk')) # 執行cmd命令
# 第一步:制作固定長度的報頭4bytes
total_size = len(res)
header = struct.pack('i',total_size)
# 第二步:把報頭發送給客戶端
conn.send(header)
# 第三步:再發送真實的數據
conn.sendall(res)
except ConnectionResetError: # 適用於windows操作系統,防止客戶端斷開連接后死循環
break
conn.close()
server.close()
client
import socket
import struct
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(client)
client.connect(('127.0.0.1', 8088))
while True:
data = input('input >>>')
if not data: # 如果數據為空,繼續輸入
continue
client.send(data.encode('GBK')) # 發送數據
# 第一步:先收報頭
header = client.recv(4)
# 第二步:從報頭中解析出對真實數據的描述信息(數據的長度)
total_size = struct.unpack('i',header)[0]
print('收到數據長度=',total_size)
# 第三步:接收真實的數據
recv_size = 0
recv_data = b''
while recv_size < total_size:
data = client.recv(1024) # 接收數據
recv_data += data
recv_size += len(data) # 不能加1024,如果加進度條,會計算有誤
print('接收數據 =', recv_data.decode('gbk', 'ignore')) # 如果設置為ignore,則會忽略非法字符;
client.close() # 關閉
輸出結果:
server端
starting...
('127.0.0.1', 13338)
客戶端的命令 dir
客戶端的命令 ipconfig/all
client端:
"C:\Program Files\Python36\python.exe" "路飛/第三模塊/第二章網絡編程/01 簡單的套接字通信/客戶端.py"
<socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
input >>>dir
收到數據長度= 477
接收數據 = 驅動器 C 中的卷是 BOOTCAMP
卷的序列號是 D471-4F4D
C:路飛\第三模塊\第二章網絡編程\01 簡單的套接字通信 的目錄
2018/07/07 14:02 <DIR> .
2018/07/07 14:02 <DIR> ..
2018/07/05 22:43 594 cmd_util.py
2018/07/07 14:02 971 客戶端.py
2018/07/07 14:01 1,673 服務端.py
3 個文件 3,238 字節
2 個目錄 28,749,410,304 可用字節
input >>>ipconfig/all
收到數據長度= 7702
接收數據 =
Windows IP 配置
主機名 . . . . . . . . . . . . . : PC
主 DNS 后綴 . . . . . . . . . . . :
節點類型 . . . . . . . . . . . . : 混合
IP 路由已啟用 . . . . . . . . . . : 否
WINS 代理已啟用 . . . . . . . . . : 否
以太網適配器 本地連接 3:
媒體狀態 . . . . . . . . . . . . : 媒體已斷開
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Bluetooth PAN Network Adapter
物理地址. . . . . . . . . . . . . : 60-F8-1D-zz-89-EF
DHCP 已啟用 . . . . . . . . . . . : 是
自動配置已啟用. . . . . . . . . . : 是
無線局域網適配器 無線網絡連接:
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Broadcom 802.11ac Network Adapter
物理地址. . . . . . . . . . . . . : 60-F8-1D-AD-zz-EE
DHCP 已啟用 . . . . . . . . . . . : 是
自動配置已啟用. . . . . . . . . . : 是
本地鏈接 IPv6 地址. . . . . . . . : fe80::55d1:e185:f929:8ce3%13(首選)
IPv4 地址 . . . . . . . . . . . . : 192.168.31.125(首選)
子網掩碼 . . . . . . . . . . . . : 255.255.255.0
獲得租約的時間 . . . . . . . . . : 2018年7月7日 9:27:54
租約過期的時間 . . . . . . . . . : 2018年7月8日 1:25:52
默認網關. . . . . . . . . . . . . : 192.168.31.1
DHCP 服務器 . . . . . . . . . . . : 192.168.31.1
DHCPv6 IAID . . . . . . . . . . . : 291567645
DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-zz-7C-0D-6E-60-F8-1D-AD-89-EE
DNS 服務器 . . . . . . . . . . . : 114.114.114.114
TCPIP 上的 NetBIOS . . . . . . . : 已啟用
以太網適配器 VirtualBox Host-Only Network:
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : VirtualBox Host-Only Ethernet Adapter
物理地址. . . . . . . . . . . . . : 0A-00-27-00-zz-13
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
本地鏈接 IPv6 地址. . . . . . . . : fe80::7d26:2c96:84f1:6c4d%19(首選)
自動配置 IPv4 地址 . . . . . . . : 169.254.108.77(首選)
子網掩碼 . . . . . . . . . . . . : 255.255.0.0
默認網關. . . . . . . . . . . . . : 192.168.56.255
DHCPv6 IAID . . . . . . . . . . . : 336199719
DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0zz60-F8-1D-AD-89-EE
DNS 服務器 . . . . . . . . . . . : fec0:0:0:ffff::1%1
fec0:0:0:ffff::2%1
fec0:0:0:ffff::3%1
TCPIP 上的 NetBIOS . . . . . . . : 已啟用
以太網適配器 VirtualBox Host-Only Network #2:
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : VirtualBox Host-Only Ethernet Adapter #2
物理地址. . . . . . . . . . . . . : 0A-00-27-00-00-14
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
本地鏈接 IPv6 地址. . . . . . . . : fe80::641c:b67e:fa43:a28d%20(首選)
IPv4 地址 . . . . . . . . . . . . : 192.168.96.1(首選)
子網掩碼 . . . . . . . . . . . . : 255.255.255.0
默認網關. . . . . . . . . . . . . :
DHCPv6 IAID . . . . . . . . . . . : 537526311
DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0D-6E-60-F8-1D-AD-89-EE
DNS 服務器 . . . . . . . . . . . : fec0:0:0:ffff::1%1
fec0:0:0:ffff::2%1
fec0:0:0:ffff::3%1
TCPIP 上的 NetBIOS . . . . . . . : 已啟用
以太網適配器 VMware Network Adapter VMnet1:
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet1
物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-01
DHCP 已啟用 . . . . . . . . . . . : 是
自動配置已啟用. . . . . . . . . . : 是
本地鏈接 IPv6 地址. . . . . . . . : fe80::20c1:b2f0:8bff:626c%25(首選)
IPv4 地址 . . . . . . . . . . . . : 192.168.109.1(首選)
子網掩碼 . . . . . . . . . . . . : 255.255.255.0
獲得租約的時間 . . . . . . . . . : 2018年7月7日 9:27:50
租約過期的時間 . . . . . . . . . : 2018年7月7日 14:27:49
默認網關. . . . . . . . . . . . . :
DHCP 服務器 . . . . . . . . . . . : 192.168.109.254
DHCPv6 IAID . . . . . . . . . . . : 385896534
DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0D-6E-60-F8-1D-AD-89-EE
DNS 服務器 . . . . . . . . . . . : fec0:0:0:ffff::1%1
fec0:0:0:ffff::2%1
fec0:0:0:ffff::3%1
TCPIP 上的 NetBIOS . . . . . . . : 已啟用
以太網適配器 VMware Network Adapter VMnet8:
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet8
物理地址. . . . . . . . . . . . . : 00-50-56zz-00-08
DHCP 已啟用 . . . . . . . . . . . : 是
自動配置已啟用. . . . . . . . . . : 是
本地鏈接 IPv6 地址. . . . . . . . : fe80::61fd:5f66:1f70:cb3d%26(首選)
IPv4 地址 . . . . . . . . . . . . : 192.168.5.1(首選)
子網掩碼 . . . . . . . . . . . . : 255.255.255.0
獲得租約的時間 . . . . . . . . . : 2018年7月7日 9:27:49
租約過期的時間 . . . . . . . . . : 2018年7月7日 14:27:48
默認網關. . . . . . . . . . . . . :
DHCP 服務器 . . . . . . . . . . . : 192.168.5.254
DHCPv6 IAID . . . . . . . . . . . : 402673750
DHCPv6 客戶端 DUID . . . . . . . : 00-01-00-01-21-7C-0D-6E-60-F8-1D-AD-89-EE
DNS 服務器 . . . . . . . . . . . : fec0:0:0:ffff::1%1
fec0:0:0:ffff::2%1
fec0:0:0:ffff::3%1
主 WINS 服務器 . . . . . . . . . : 192.168.5.2
TCPIP 上的 NetBIOS . . . . . . . : 已啟用
隧道適配器 本地連接* 14:
媒體狀態 . . . . . . . . . . . . : 媒體已斷開
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #2
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
隧道適配器 Teredo Tunneling Pseudo-Interface:
媒體狀態 . . . . . . . . . . . . : 媒體已斷開
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Teredo Tunneling Pseudo-Interface
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
隧道適配器 isatap.{0DA4A980-7247-4922-AAFB-55760B865C15}:
媒體狀態 . . . . . . . . . . . . : 媒體已斷開
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #3
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
隧道適配器 isatap.localdomain:
媒體狀態 . . . . . . . . . . . . : 媒體已斷開
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #5
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
隧道適配器 本地連接* 15:
媒體狀態 . . . . . . . . . . . . : 媒體已斷開
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #6
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
隧道適配器 isatap.{94C5F926-3E20-4589-A88E-54A36934D42C}:
媒體狀態 . . . . . . . . . . . . : 媒體已斷開
連接特定的 DNS 后綴 . . . . . . . :
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #8
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已啟用 . . . . . . . . . . . : 否
自動配置已啟用. . . . . . . . . . : 是
input >>>
解決粘包問題優化版(適用於傳輸字節很大)
server端
import socket
import subprocess
import struct
import json
def cmd_exec(cmd):
"""
執行shell命令
:param cmd:
:return:
"""
p = subprocess.Popen(cmd, shell=True,
stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
return stderr
return stdout
sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用地址端口
sock_server.bind(('127.0.0.1', 8088))
sock_server.listen(1) # 開始監聽,1代表在允許有一個連接排隊,更多的新連接連進來時就會被拒絕
print('starting...')
while True:
conn, client_addr = sock_server.accept() # 阻塞直到有連接為止,有了一個新連接進來后,就會為這個請求生成一個連接對象
print(client_addr)
while True:
try:
data = conn.recv(1024) # 接收1024個字節
if not data: break # 適用於linux操作系統,防止客戶端斷開連接后死循環
print('客戶端的命令', data.decode('gbk'))
res = cmd_exec(data.decode('gbk')) # 執行cmd命令
# 第一步:制作固定長度的報頭dict
header_dict ={
'filename':'文件名',
'md5':'md5值',
'total_size':len(res)
}
header_json = json.dumps(header_dict, ensure_ascii='False',indent=2) # 序列化json
print(header_json)
header_bytes = header_json.encode('utf-8')
header = struct.pack('i', len(header_bytes))
# 第二步:把報頭長度發送給客戶端
conn.send(header)
# 第三步:把報頭內容發送給客戶端
conn.send(header_bytes)
# 第四步:再發送真實的數據
conn.sendall(res)
except ConnectionResetError: # 適用於windows操作系統,防止客戶端斷開連接后死循環
break
conn.close()
server.close()
client端
import socket
import struct
import json
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print(client)
client.connect(('127.0.0.1', 8088))
while True:
data = input('input >>>')
if not data: # 如果數據為空,繼續輸入
continue
client.send(data.encode('GBK')) # 發送數據
# 第一步:先收報頭
header = client.recv(4)
# 第二步:從報頭中解析(header數據的長度)
header_size = struct.unpack('i',header)[0]
print('收到報頭長度=', header_size)
# 第三步:收到報頭解析出對真實數據的描述信息
header_json = client.recv(header_size)
header_dict = json.loads(header_json)
print('收到報頭內容=',header_dict)
total_size = header_dict['total_size']
# 第三步:接收真實的數據
recv_size = 0
recv_data = b''
while recv_size < total_size:
data = client.recv(1024) # 接收數據
recv_data += data
recv_size += len(data) # 不能加1024,如果加進度條,會計算有誤
print('接收數據 =', recv_data.decode('gbk', 'ignore')) # 如果設置為ignore,則會忽略非法字符;
client.close() # 關閉
結果
server端
starting...
('127.0.0.1', 15685)
客戶端的命令 ls
{
"filename": "\u6587\u4ef6\u540d",
"md5": "md5\u503c",
"total_size": 61
}
客戶端的命令 dir
{
"filename": "\u6587\u4ef6\u540d",
"md5": "md5\u503c",
"total_size": 477
}
client端
<socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
input >>>ls
收到報頭長度= 80
收到報頭內容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 61}
接收數據 = 'ls' 不是內部或外部命令,也不是可運行的程序
或批處理文件。
input >>>dir
收到報頭長度= 81
收到報頭內容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 477}
接收數據 = 驅動器 C 中的卷是 BOOTCAMP
卷的序列號是 D471-4F4D
簡單的套接字通信 的目錄
2018/07/07 14:51 <DIR> .
2018/07/07 14:51 <DIR> ..
2018/07/05 22:43 594 cmd_util.py
2018/07/07 14:51 1,204 客戶端.py
2018/07/07 14:51 2,098 服務端.py
3 個文件 3,896 字節
2 個目錄 28,694,999,040 可用字節
input >>>ipconfig/all
收到報頭長度= 82
收到報頭內容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 7702}
接收數據 =
Windows IP 配置
……
