C/S架构和Soket
C/S架构即客户端/服务器架构,包括软件(web应用)和硬件(比如打印机)两种架构方式。客户端和服务端的通信基于网络协议,而Socket套接字将复杂的网络协议(TCP/IP、UDP/IP协议族)封装起来,提供一组简单的接口供使用者调用。因此基于Socket,我们能方便地开发符合网络协议标准的客户端和服务端软件。
套接字家族
AF_UNIX > 地址家族:UNIX,基于文件的套接字。
AF_INET > 地址家族:Internet,基于网络的套接字,网络编程用。
套接字地址
基于IP地址和PORT端口,可以唯一定位网络中的应用程序。
套接字类型
TCP套接字
TCP套接字是面向连接的,即在通讯之前一定要建立一条连接。也被称为“流套接字”。要创建TCP套接字时,指定类型为SOCK_STREAM。
UDP套接字
UDP数据报(datagram)型的无连接套接字。要创建UDP套接字时,指定类型为SOCK_DGRAM。
socket模块
导入模块:from socket import *
将socket模块内的所有属性导入当前名称空间。
创建套接字
使用socket模块中的socket函数:
socket(socket_family, socket_type)
创建一个TCP套接字:tcpsock = socket(AF_INET, SOCK_STEAM)
创建一个UDP套接字:tcpsock = socket(AF_INET, SOCK_DGRAM)
创建了套接字后,所有的交互都通过调用该套接字的方法来进行。
套接字方法
模拟xshell远程服务
这里用TCP套接字连接,来模拟Xshell服务。
服务端:
from socket import * # 尽量不要用这种导入方式,导入多个模块容易方法重名
import subprocess
tcpser = socket(AF_INET,SOCK_STREAM) # 拿到一部手机
tcpser.setsockopt(SOL_SOCKET, SO_REUSEADDR,1) # 重用ip和端口,解决重启服务时ip和端口占用问题。
tcpser.bind(('',8080)) # 给手机插卡
tcpser.listen(5) # 开机
while True: # 连接循环
print('waitting for connection...')
conn, addr = tcpser.accept() # 等电话 (拿到conn链连接对象,和客户端地址)
print('connected from',addr)
while True: # 通讯循环
try:
cmd = conn.recv(1024) # conn是一个链接,基于客户端和服务端的。注意,接收和发送必须是bytes
# 如果客户端异常断开,那么服务端的基于conn的conn.recv就会抛出异常,不过不做处理,OS终止程序运行,服务端崩了!
# 对于这种不可避免的异常,就需要异常处理。
if not cmd:
print('客户端已正常断开连接!')
break
s = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
msg = s.stdout.read() + s.stderr.read() # subprocess的输出默认是bytes
if not msg:
conn.send('无返回内容'.encode('gbk'))
else:
conn.send(msg)
except Exception:
# 捕获到异常,结束当前通讯循环,关闭当前连接,释放OS资源。回到连接循环,等待连接到来。
print('客户端异常断开!')
break
conn.close() # 挂电话
tcpser.close() # 关机
客户端:
import socket
tcpclient = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcpclient.connect(('localhost',8080))
while True:
cmd = input('>>> ').strip()
if not cmd: continue # 因为消息放到缓存中,由OS根据网络协议来发送。输入为空的话,OS将不会发送信息。
if cmd == 'quit': break # 客户端正常退出
tcpclient.send(cmd.encode('utf-8')) # 这里的send是发到缓存中
print('已发送请求')
msg = tcpclient.recv(1024) # 从缓存中收
print('已进接收返回内容')
print(msg.decode('gbk'))
tcpclient.close()
UDP套接字与TCP套接字的特点及应用场景
TCP粘包问题
粘包的原因是程序无法判断数据的边界,造成数据多收了。
上面的模拟的Xshell服务,如果一条命令返回的内容很大,超过recv()一次收取的最大值,那么一次取不完。再输入第二天命令后,
解决方案:让程序知道收取数据的长度,按长度收取。
方案一:
发送数据时分两次:第一次发送数据长度,第二次发送真实数据。中间有recv()来强制更新缓存。
方案二:
循环收取过长的数据
套接字对象从缓存里面取数据,一次取的数据最好不要超过缓存的大小。
客户端:
from socket import *
c = socket(AF_INET,SOCK_STREAM)
c.connect_ex(('127.0.01',8080))
while True:
msg = input('>>> ').strip()
if not msg: continue
if msg == 'quit':break
try:
c.send(msg.encode('utf-8')) # 如果服务器关闭,c.send()异常
length=int(c.recv(1024).decode('utf-8'))
print('信息长度是:',length)
c.send('recv_ready'.encode('utf-8'))
# tcp收数据,一次最好不要超过缓存的大小(linux默认8kb),这里我们设置循环收取,每次1kb,最后拼接。
recv_size=0
total_data=b''
while recv_size < length:
if length - recv_size < 1024: # 判断最后一次收数据时的大小。
# 如果最后一部分数据小于1kb,而后面又有其它数据,仍按照1kb收取,就可能粘包
data = c.recv(length - recv_size)
else:
data = c.recv(1024) # 最大收取1024,缓存内数据不足时还是会按实际收取的。
total_data += data
recv_size += len(data) # 按数据的实际接收长度加。
# 因为数据长度可能不足1024,或者数据很长,分段来收,最后一次收的可能小于1024
print(data.decode('gbk'))
except Exception:
break
c.close()
ftp服务模拟
客户端:
#! user/bin/env python
# -*- coding: utf-8 -*-
# __author__ = 'Ayhan_Huang'
# Date = 2017/7/11
#客户端:
# 发送请求,
# 接收返回,
# 保存
import socket
import json
import os
import hashlib
ftp_cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ftp_cli.connect_ex(('192.168.1.104',8086))
def pack_header(header, header_size):
if len(bytes(json.dumps(header),encoding='utf-8')) < header_size:
header['fill'].zfill(header_size - len(bytes(json.dumps(header),encoding='utf-8')))
return bytes(json.dumps(header),encoding='utf-8')
while True:
request = input('>>> ').strip()
if not request: continue
if request == 'quit': break
method, filename = request.split()
# try:
if method == 'get':
ftp_cli.send(request.encode('utf-8'))
b_header = ftp_cli.recv(1024)
header = json.loads(b_header.decode('utf-8'))
if header.get('error'):
print(header.get('error'))
else: # 服务端存在文件,保存到本地
filename = header['filename']
file_size = header['file_size']
f = open(filename,'wb')
m = hashlib.md5()
recv_size = 0
while recv_size < file_size:
if file_size - recv_size < 8192:
data = ftp_cli.recv(file_size - recv_size)
else:
data = ftp_cli.recv(8192)
m.update(data)
f.write(data)
recv_size += len(data)
else:
print('received all from server')
print('file md5_value:',m.hexdigest())
f.close()
if method == 'post':
#先判断要上传的文件存在
if os.path.isfile(filename):
#制作定长文件头发过去。相当于服务端从客户端下载文件。
ftp_cli.send(request.encode('utf-8'))
header = {}
header['filename'] = filename
header['file_size'] = os.stat(filename).st_size
header['fill'] = ''
b_header = pack_header(header, 300)
ftp_cli.send(b_header)
m = hashlib.md5()
f = open(filename,'rb')
for line in f:
m.update(line)
ftp_cli.send(line)
else:
print('post finished')
print('file md5_value is: ',m.hexdigest())
f.close()
else:
print('post file no exists')
# else:
# ftp_cli.send(request.encode('utf-8'))
# b_header = ftp_cli.recv(1024)
# header = json.loads(b_header.decode('utf-8'))
#
# except Exception:
# break
ftp_cli.close()
服务端:
#! user/bin/env python
# -*- coding: utf-8 -*-
# __author__ = 'Ayhan_Huang'
# Date = 2017/7/11
# 服务端:
# 1.接收请求
# 2.解析指令
# 2.1 判断get post 2.2 找文件 或 新建文件 2.3 制作消息头,返回 2.4 发送 或 接收
import socket
import json
import hashlib
import os
ftp_ser = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ftp_ser.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1) # 重用ip和端口,解决重启服务时ip和端口占用问题。
ftp_ser.bind(('192.168.16.28',8080))
ftp_ser.listen(5)
def pack_header(header, header_size):
if len(bytes(json.dumps(header),encoding='utf-8')) < header_size:
header['fill'].zfill(header_size - len(bytes(json.dumps(header),encoding='utf-8')))
return bytes(json.dumps(header),encoding='utf-8')
while True: # 连接循环
print('waitting for connection...')
conn, addr = ftp_ser.accept()
print('connected from',addr)
while True: # 通讯循环
# try: # 用了try异常处理,程序倒是不崩了,但是出错了都不知道原因。。。因此,调试程序时,建议注释掉。
request = conn.recv(1024)
method, filename = request.decode('utf-8').split()
if method == 'get':
if os.path.isfile(filename): # 判断文件存在
# 制作定长消息头
header = {}
header['filename'] = filename
header['file_size'] = os.stat(filename).st_size
header['fill'] = ''
b_header = pack_header(header, 300)
conn.send(b_header)
m=hashlib.md5()
f = open(filename, 'rb')
for line in f:
m.update(line)
conn.send(line)
else: # 上面的循环正常结束
print('send finished')
print('file md5_value is: ',m.hexdigest())
f.close()
else:
header['error'] = 'cannot find %s on server' % filename
b_header = pack_header(header, 300)
conn.send(b_header)
if method == 'post':
b_header = conn.recv(1024)
header = json.loads(b_header.decode('utf-8'))
filename = header['filename']
file_size = header['file_size']
print('will receive:',filename,'size:',file_size)
m = hashlib.md5()
f = open(filename,'wb')
recv_size = 0
while recv_size < file_size:
if file_size - recv_size < 8192:
data = conn.recv(file_size - recv_size)
else:
data = conn.recv(8192)
m.update(data)
f.write(data)
recv_size += len(data)
else:
print('received all from client')
print('file md5_value: ',m.hexdegitst())
f.close()
#
# else:
# header['method_error'] = 'method unsurported'
# b_header = pack_header(header, 300)
# conn.send(b_header)
# except Exception:
# break
conn.close()
tcpser.close()