軟件開發(fā)的架構(gòu)
1况既、C/S架構(gòu)
C/S即:Client與Server 这溅,中文意思:客戶端與服務(wù)器端架構(gòu)组民,這種架構(gòu)也是從用戶層面(也可以是物理層面)來劃分的棒仍。
這里的客戶端一般泛指客戶端應(yīng)用程序EXE,程序需要先安裝后臭胜,才能運(yùn)行在用戶的電腦上莫其,對用戶的電腦操作系統(tǒng)環(huán)境依賴較大。
2耸三、B/S架構(gòu)
B/S即:Browser與Server,中文意思:瀏覽器端與服務(wù)器端架構(gòu)乱陡,這種架構(gòu)是從用戶層面來劃分的。
Browser瀏覽器仪壮,其實(shí)也是一種Client客戶端憨颠,只是這個客戶端不需要大家去安裝什么應(yīng)用程序,只需在瀏覽器上通過HTTP請求服務(wù)器端相關(guān)的資源(網(wǎng)頁資源)积锅,客戶端Browser瀏覽器就能進(jìn)行增刪改查爽彤。
Socket 概念
Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層,它是一組接口缚陷。在設(shè)計模式中适篙,Socket其實(shí)就是一個門面模式,它把復(fù)雜的TCP/IP協(xié)議族隱藏在Socket接口后面箫爷,對用戶來說嚷节,一組簡單的接口就是全部聂儒,讓Socket去組織數(shù)據(jù),以符合指定的協(xié)議硫痰。
tcp和udp協(xié)議
TCP(Transmission Control Protocol) 可靠的衩婚,面向連接的協(xié)議。應(yīng)用:web瀏覽器碍论,電子郵件谅猾,文件傳輸程序。
UDP(User Datagram Protocol)不可靠的鳍悠,無連接的服務(wù)税娜,傳輸效率高,一對一藏研、一對多敬矩、多對一、多對多蠢挡、面向報文弧岳,盡最大努力服務(wù),無擁塞控制业踏。應(yīng)用:視頻流禽炬,IP語音。
套接字工作流程
先從服務(wù)端說起勤家。服務(wù)器端先初始化Socket腹尖,然后與端口綁定(bind),對端口進(jìn)行監(jiān)聽(listen),調(diào)用accept阻塞伐脖,等待客戶端連接热幔。在這時如果有個客戶端初始化一個Socket,然后連接服務(wù)器(connect)讼庇,如果連接成功绎巨,這時客戶端與服務(wù)器端的連接就建立了∪渥模客戶端發(fā)送數(shù)據(jù)請求场勤,服務(wù)器端接收請求并處理請求,然后把回應(yīng)數(shù)據(jù)發(fā)送給客戶端歼跟,客戶端讀取數(shù)據(jù)和媳,最后關(guān)閉連接,一次交互結(jié)束嘹承。
基于TCP協(xié)議的套接字
tcp是基于鏈接的窗价,因此必須先啟動服務(wù)端,然后再啟動客戶端去鏈接服務(wù)端
server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898)) #把地址綁定到套接字
sk.listen() #監(jiān)聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024) #接收客戶端信息
print(ret) #打印客戶端信息
conn.send(b'hi') #向客戶端發(fā)送信息
conn.close() #關(guān)閉客戶端套接字
sk.close() #關(guān)閉服務(wù)器套接字(可選)
client端
import socket
sk = socket.socket() # 創(chuàng)建客戶套接字
sk.connect(('127.0.0.1',8898)) # 嘗試連接服務(wù)器
sk.send(b'hello!')
ret = sk.recv(1024) # 對話(發(fā)送/接收)
print(ret)
sk.close() # 關(guān)閉客戶套接字
若在重啟服務(wù)端看到以下錯誤:
解決方案:
#加入一條socket配置叹卷,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它撼港,在bind前加
sk.bind(('127.0.0.1',8898)) #把地址綁定到套接字
sk.listen() #監(jiān)聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024) #接收客戶端信息
print(ret) #打印客戶端信息
conn.send(b'hi') #向客戶端發(fā)送信息
conn.close() #關(guān)閉客戶端套接字
sk.close() #關(guān)閉服務(wù)器套接字(可選)
基于UDP的套接字
udp是無鏈接的坪它,先啟動哪一端都不會報錯
server端
1 ss = socket() #創(chuàng)建一個服務(wù)器的套接字
2 ss.bind() #綁定服務(wù)器套接字
3 inf_loop: #服務(wù)器無限循環(huán)
4 cs = ss.recvfrom()/ss.sendto() # 對話(接收與發(fā)送)
5 ss.close() # 關(guān)閉服務(wù)器套接字
client端
cs = socket() # 創(chuàng)建客戶套接字
comm_loop: # 通訊循環(huán)
cs.sendto()/cs.recvfrom() # 對話(發(fā)送/接收)
cs.close() # 關(guān)閉客戶套接字
粘包現(xiàn)象
什么是粘包
須知:只有TCP有粘包現(xiàn)象,UDP永遠(yuǎn)不會粘包
首先要掌握一個socket收發(fā)消息的原理
發(fā)送端可以使1k1k的發(fā)送數(shù)據(jù)帝牡,而接收端的應(yīng)用程序可以2k2k地提走數(shù)據(jù)往毡,當(dāng)然也有可能一次提走3k或6k的數(shù)據(jù),或者一次提走幾個字節(jié)的數(shù)據(jù)靶溜,也就是說應(yīng)用程序所看到的數(shù)據(jù)是一個整體开瞭,或者是一個流(stream),一條消息有多少字節(jié)對應(yīng)用程序是不可見的罩息,因此TCP協(xié)議是面向流的協(xié)議嗤详,這也是容易出現(xiàn)粘包的原因。
而UDP是面向消息的協(xié)議瓷炮,每個udp都是一條消息葱色,應(yīng)用程序必須以消息為單位提取數(shù)據(jù),不能一次提取任意字節(jié)的數(shù)據(jù)娘香,這一點(diǎn)和TCP是很不同的苍狰。
所謂粘包問題主要還是接收方不知道消息之間的界限,不知道一次性提取多少字節(jié)的數(shù)據(jù)所造成的烘绽。
此外淋昭,發(fā)送方因此的粘包是由TCP協(xié)議本身造成的,TCP為提高傳輸效率安接,發(fā)送方往往要收集到足夠多的數(shù)據(jù)才發(fā)送一個TCP段翔忽,若連續(xù)幾次需要send的數(shù)據(jù)都很少,通常TCP會根據(jù)優(yōu)化算法把這些數(shù)據(jù)合成一個TCP字段后一次發(fā)送出去赫段,這樣接收方就收到了粘包數(shù)據(jù)呀打。
1矢赁、TCP(transport control protocol糯笙,傳輸控制協(xié)議)是面向連接的,面向流的撩银,提供高可靠性服務(wù)给涕。收發(fā)兩端(客戶端和服務(wù)器端)都要有一一成對的socket,因此额获,發(fā)送端為了將多個發(fā)往接收端的包更有效的發(fā)到對方够庙,使用了優(yōu)化算法(Nagle算法),將多次間隔較小且數(shù)據(jù)量小的數(shù)據(jù)抄邀,合并成一個大的數(shù)據(jù)塊耘眨,然后進(jìn)行封包。這樣接收端就很難分辨出來了境肾,必須提供科學(xué)的拆包機(jī)制剔难。即面向流的通信是無消息保護(hù)邊界的胆屿。
2、UDP(user datagram protocol偶宫,用戶數(shù)據(jù)報協(xié)議)是無連接的非迹,面向消息的,提供高效率服務(wù)纯趋。不會使用塊的合并優(yōu)化算法憎兽,, 由于UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區(qū))采用了鏈?zhǔn)浇Y(jié)構(gòu)來記錄每一個到達(dá)的UDP包吵冒,在每個UDP包中就有了消息頭(消息來源地址纯命,端口等信息),這樣痹栖,對于接收端來說扎附,就容易進(jìn)行區(qū)分處理了。 即面向消息的通信是有消息保護(hù)邊界的结耀。
3留夜、tcp是基于數(shù)據(jù)流的,于是收發(fā)的消息不能為空图甜,這就需要在客戶端和服務(wù)端都添加空消息的處理機(jī)制碍粥,防止程序卡住,而udp是基于數(shù)據(jù)報的黑毅,即便是你輸入的是空內(nèi)容(直接回車)嚼摩,那也不是空消息,udp協(xié)議會幫你封裝上消息頭
兩種情況下會發(fā)生粘包
發(fā)送端需要等緩沖區(qū)滿才發(fā)送出去矿瘦,造成粘包(發(fā)送數(shù)據(jù)時間間隔很短枕面,數(shù)據(jù)了很小,會合到一起缚去,產(chǎn)生粘包)
server端
#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(10)
data2=conn.recv(10)
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()
服務(wù)端
client端
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
客戶端
接收端不及時接收緩沖區(qū)的包潮秘,造成多個包接收(客戶端發(fā)送了一段數(shù)據(jù),服務(wù)端只接受了一小部分易结,服務(wù)端下次再接收的時候還是從緩沖區(qū)拿上次遺留的數(shù)據(jù)枕荞,產(chǎn)生粘包)
server端
#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(2) #一次沒有收完整
data2=conn.recv(10)#下次收的時候,會先取舊的數(shù)據(jù),然后取新的
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()
服務(wù)端
client端
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello feng'.encode('utf-8'))
客戶端
解決粘包的方法
struct模塊
import json,struct
#假設(shè)通過客戶端上傳1T:1073741824000的文件a.txt
#為避免粘包,必須自定制報頭
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T數(shù)據(jù),文件路徑和md5值
#為了該報頭能傳送,需要序列化并且轉(zhuǎn)為bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并轉(zhuǎn)成bytes,用于傳輸
#為了讓客戶端知道報頭的長度,用struck將報頭長度這個數(shù)字轉(zhuǎn)成固定長度:4個字節(jié)
head_len_bytes=struct.pack('i',len(head_bytes)) #這4個字節(jié)里只包含了一個數(shù)字,該數(shù)字是報頭的長度
#客戶端開始發(fā)送
conn.send(head_len_bytes) #先發(fā)報頭的長度,4個bytes
conn.send(head_bytes) #再發(fā)報頭的字節(jié)格式
conn.sendall(文件內(nèi)容) #然后發(fā)真實(shí)內(nèi)容的字節(jié)格式
#服務(wù)端開始接收
head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的字節(jié)格式
x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度
head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式
header=json.loads(json.dumps(header)) #提取報頭
#最后根據(jù)報頭的內(nèi)容提取真實(shí)的數(shù)據(jù),比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
struct詳細(xì)用法
#_*_coding:utf-8_*_
#http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
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))
關(guān)于struct的詳細(xì)用法
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))) #先發(fā)back_msg的長度
conn.sendall(back_msg) #在發(fā)真實(shí)的內(nèi)容
conn.close()
服務(wù)端(自定制報頭)
client端(自定制報頭)
#_*_coding:utf-8_*_
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默認(rèn)gbk編碼
客戶端(自定制報頭)
我們可以把報頭做成字典躏精,字典里包含將要發(fā)送的真實(shí)數(shù)據(jù)的詳細(xì)信息,然后json序列化鹦肿,然后用struck將序列化后的數(shù)據(jù)長度打包成4個字節(jié)(4個自己足夠用了)
發(fā)送時:
先發(fā)報頭長度
再編碼報頭內(nèi)容然后發(fā)送
最后發(fā)真實(shí)內(nèi)容
接收時:
先手報頭長度矗烛,用struct取出來
根據(jù)取出的長度收取報頭內(nèi)容,然后解碼箩溃,反序列化
從反序列化的結(jié)果中取出待取數(shù)據(jù)的詳細(xì)信息瞭吃,然后去取真實(shí)的數(shù)據(jù)內(nèi)容