1.网络基础
参考:http://www.cnblogs.com/Eva-J/articles/8066842.html
2.软件开发架构
C/S架构:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。
B/S架构:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。(其实B/S架构也是一种C/S架构)
3.计算机网络
1.在网络中的两个程序通过IP和PORT找到对方
IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。
"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。
2.socket概念
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
3.套接字(socket)介绍
-- 套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
-- 套接字的分类
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
4.传输协议价绍
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
我知道说这些你们也不懂,直接上图。
4.套接字(socket)的使用
4.1基于TCP协议的socket
特点:TCP链接服务端同一时间只能与一个客户端进行通信,服务端与客户端一旦建立连接,将一直占用socket链接
1- 简单版TCP通信
server端
import socket # sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk = socket.socket() # 拿到一个socket实例化对象,默认是基于网络类型的套接字家族,类型是TCP sk.bind(('127.0.0.1', 9000)) # 把地址绑定到套接字 sk.listen() # 监听链接 conn, addr = sk.accept() # 接收客户端链接 print(addr) # ('127.0.0.1', 51625) 客户端的地址 conn.send('haha'.encode('utf-8')) # 向客户端发送消息,消息是bytes类型 ret = conn.recv(1024).decode('utf-8') # 接收客户端消息,需要解码 1024--最大接收1024个字节 print(ret) # 打印客户端消息 conn.close() # 关闭客户端套接字 sk.close() # 关闭服务器套接字
client端
import socket # sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk = socket.socket() # 拿到一个socket实例化对象,默认是基于网络类型的套接字家族,类型是TCP sk.connect(('127.0.0.1', 9000)) # 尝试连接服务器 ret = sk.recv(1024).decode('utf-8') # 陷入阻塞,客户端接收消息,并解码 1024--最大接收1024个字节 print(ret) sk.send('hahahaha'.encode('utf-8')) # 客户端发送消息 sk.close() #关闭客户端套接字
2- 异常处理 -- 端口被占用
# 异常描述 # sk.bind(('127.0.0.1', 9000)) # 把地址绑定到套接字 # OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
# 解决方法: # 加入一条socket配置,重用ip和端口 import socket from socket import SOL_SOCKET,SO_REUSEADDR # sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk = socket.socket() # 拿到一个socket实例化对象,默认是基于网络类型的套接字家族,类型是TCP sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) ####### 就是它,在bind前加 sk.bind(('127.0.0.1', 9000)) # 把地址绑定到套接字 sk.listen() # 监听链接 conn, addr = sk.accept() # 接收客户端链接 print(addr) # ('127.0.0.1', 51625) 客户端的地址 conn.send('haha'.encode('utf-8')) # 向客户端发送消息,消息是bytes类型 ret = conn.recv(1024).decode('utf-8') # 接收客户端消息,需要解码 print(ret) # 打印客户端消息 conn.close() # 关闭客户端套接字 sk.close() # 关闭服务器套接字
3- 单间循环版TCP通信
server端
import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() conn, addr = sk.accept() print('%s正在与你通信'%addr[0]) while True: msg = input('>>> ') conn.send(msg.encode('utf-8')) ret = conn.recv(1024).decode('utf-8') print(ret) conn.close() sk.close()
client端
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9000)) while True: ret = sk.recv(1024).decode('utf-8') print(ret) msg = input('>>> ') sk.send(msg.encode('utf-8')) sk.close()
4- 带退出的循环版TCP通信
server端
import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() conn, addr = sk.accept() print('%s正在与你通信'%addr[0]) while True: msg = input('>>> ') conn.send(msg.encode('utf-8')) if msg == 'q':break ret = conn.recv(1024).decode('utf-8') print(ret) if ret == 'q':break conn.close() sk.close()
client端
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9000)) while True: ret = sk.recv(1024).decode('utf-8') print(ret) if ret == 'q':break msg = input('>>> ') sk.send(msg.encode('utf-8')) if msg == 'q':break sk.close()
5- 一个服务端对应多个客户端,但一次只能与一个客户端通信,当这个客户端断开连接了,依次与后面的客户端进行通信
server端
import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() while True: conn, addr = sk.accept() print('%s正在与你通信'%addr[0]) while True: msg = input('>>> ') conn.send(msg.encode('utf-8')) if msg == 'q':break ret = conn.recv(1024).decode('utf-8') print(ret) if ret == 'q':break conn.close() sk.close()
client端
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9000)) while True: ret = sk.recv(1024).decode('utf-8') print(ret) if ret == 'q':break msg = input('>>> ') sk.send(msg.encode('utf-8')) if msg == 'q':break sk.close()
6- 简单版ssh,windows版
server端
import socket import subprocess ip_port = ('127.0.0.1', 9000) BUFSIZE = 1024 sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sk.bind(ip_port) sk.listen() while True: conn,addr = sk.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 os import socket BUFSIZE=1024 ip_port = ('127.0.0.1', 9000) sk = socket.socket() sk.connect_ex(ip_port) while True: cmd = input(os.getcwd() + '> ') if not cmd:continue sk.send(cmd.encode('utf-8')) res = sk.recv(BUFSIZE).decode('gbk') print(res)
演示
# server 客户端: ('127.0.0.1', 60670) # client E:\python学习目录\模拟ssh> dir 驱动器 E 中的卷是 新加卷 卷的序列号是 3C42-57C9 E:\python学习目录\模拟ssh 的目录 2018/06/09 15:09 <DIR> . 2018/06/09 15:09 <DIR> .. 2018/06/09 15:09 288 client.py 2018/06/09 15:09 1,032 server.py 2018/06/09 14:51 0 __init__.py 3 个文件 1,320 字节 2 个目录 374,906,806,272 可用字节
7- 使用struct解决粘包问题
server端
import socket import struct import subprocess ip_port = ('127.0.0.1', 9000) BUFSIZE = 1024 sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) sk.bind(ip_port) sk.listen() while True: conn,addr = sk.accept() print('客户端:', addr) while True: cmd_len = conn.recv(4) cmd = conn.recv(struct.unpack('i', cmd_len)[0]) res = subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr = res.stderr.read() print('stderr----', stderr) conn.send(stderr) stdout = res.stdout.read() print('stdout----', stdout) conn.send(stdout)
client端
import os import socket import struct BUFSIZE=1024 ip_port = ('127.0.0.1', 9000) sk = socket.socket() sk.connect_ex(ip_port) while True: cmd = input(os.getcwd() + '> ') if not cmd: continue cmd_len = len(cmd) bytes_cmd = struct.pack('i', cmd_len) sk.send(bytes_cmd) sk.send(cmd.encode('utf-8')) res = sk.recv(BUFSIZE).decode('gbk') print(res)
演示
# server 客户端: ('127.0.0.1', 52647) stderr---- b'' stdout---- b' \xc7\xfd\xb6\xaf\xc6\xf7 E \xd6\xd0\xb5\xc4\xbe\xed\xca\xc7 \xd0\xc2\xbc\xd3\xbe\xed\r\n \xbe\xed\xb5\xc4\xd0\xf2\xc1\xd0\xba\xc5\xca\xc7 3C42-57C9\r\n\r\n E:\\python\xd1\xa7\xcf\xb0\xc4\xbf\xc2\xbc\\python\xd1\xa7\xcf\xb0\xd6\xae\xc2\xc3\\\xd1\xa7\xcf\xb0\xd6\xae\xc2\xc3\\day08 \xb8\xb4\xcf\xb0\\\xc4\xa3\xc4\xe2ssh \xb5\xc4\xc4\xbf\xc2\xbc\r\n\r\n2018/06/09 15:30 <DIR> .\r\n2018/06/09 15:30 <DIR> ..\r\n2018/06/09 15:26 288 c1.py\r\n2018/06/09 15:23 395 client.py\r\n2018/06/09 15:26 715 s1.py\r\n2018/06/09 15:30 853 server.py\r\n2018/06/09 14:51 0 __init__.py\r\n 5 \xb8\xf6\xce\xc4\xbc\xfe 2,251 \xd7\xd6\xbd\xda\r\n 2 \xb8\xf6\xc4\xbf\xc2\xbc 374,906,802,176 \xbf\xc9\xd3\xc3\xd7\xd6\xbd\xda\r\n' stderr---- b"'sl' \xb2\xbb\xca\xc7\xc4\xda\xb2\xbf\xbb\xf2\xcd\xe2\xb2\xbf\xc3\xfc\xc1\xee\xa3\xac\xd2\xb2\xb2\xbb\xca\xc7\xbf\xc9\xd4\xcb\xd0\xd0\xb5\xc4\xb3\xcc\xd0\xf2\r\n\xbb\xf2\xc5\xfa\xb4\xa6\xc0\xed\xce\xc4\xbc\xfe\xa1\xa3\r\n" stdout---- b'' # client E:\python学习目录\模拟ssh> dir 驱动器 E 中的卷是 新加卷 卷的序列号是 3C42-57C9 E:\python学习目录\模拟ssh 的目录 2018/06/09 15:30 <DIR> . 2018/06/09 15:30 <DIR> .. 2018/06/09 15:26 288 c1.py 2018/06/09 15:23 395 client.py 2018/06/09 15:26 715 s1.py 2018/06/09 15:30 853 server.py 2018/06/09 14:51 0 __init__.py 5 个文件 2,251 字节 2 个目录 374,906,802,176 可用字节 E:\python学习目录\模拟ssh> sl 'sl' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
4.2基于UDP协议的socket
特点:没有建立链接的过程,可以同时接受多个客户端的链接
1- 简单版UDP通信
server端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # ******-******必须添加协议类型 sk.bind(('127.0.0.1', 9000)) # 绑定服务器套接字 msg, addr = sk.recvfrom(1024) print(msg.decode('utf-8')) sk.sendto(b'byebye', addr) # 需要添加客户端的地址 sk.close()
client端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # ******-******必须添加协议类型 sk.sendto(b'hello', ('127.0.0.1', 9000)) msg,addr = sk.recvfrom(1024) print(msg.decode('utf-8')) sk.close()
2- 循环版支持多个客户端UDP通信
server端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型 sk.bind(('127.0.0.1', 9000)) # 绑定服务器套接字 while True: msg, addr = sk.recvfrom(1024) print(addr,':',msg.decode('utf-8')) inp = input('>>> ') sk.sendto(inp.encode('utf-8'), addr) # 需要添加客户端的地址 sk.close()
client端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型 inp = input('>>> ') sk.sendto(inp.encode('utf-8'), ('127.0.0.1', 9000)) msg,addr = sk.recvfrom(1024) print(msg.decode('utf-8')) sk.close()
演示
# server: ('127.0.0.1', 59120) : 你好 >>> 不好 ('127.0.0.1', 53072) : 好不好 >>> 不太好 # client1: >>> 你好 不好 # client2: >>> 好不好 不太好
3- 并发循环版UDP通信
server端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型 sk.bind(('127.0.0.1', 9000)) # 绑定服务器套接字 while True: msg, addr = sk.recvfrom(1024) print(addr,':',msg.decode('utf-8')) inp = input('>>> ') sk.sendto(inp.encode('utf-8'), addr) # 需要添加客户端的地址 sk.close()
client端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) # 必须添加协议类型 while True: inp = input('>>> ') sk.sendto(inp.encode('utf-8'), ('127.0.0.1', 9000)) msg,addr = sk.recvfrom(1024) print(msg.decode('utf-8')) sk.close()
演示
# server: ('127.0.0.1', 55844) : 哈哈哈 >>> 不哈 ('127.0.0.1', 59756) : 2哈2哈 >>> 不2哈 # client1: >>> 哈哈哈 不哈 >>> # client2: >>> 2哈2哈 不2哈 >>>
4- ntp时钟服务
server端
import socket from time import strftime ip_port = ('127.0.0.1', 9000) bufsize = 1024 sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind(ip_port) while True: msg, addr = sk.recvfrom(bufsize) msg = msg.decode('utf-8') if msg == 'ntpdate': print('%s 正在与你进行时间同步'%addr[0]) time_fmt = '%Y-%m-%d %X' back_msg = strftime(time_fmt) sk.sendto(back_msg.encode('utf-8'), addr) sk.close()
client端
import socket import time ip = input('请输入时钟服务器的IP:') while True: port = input('请输入时间同步端口:') if port.isdigit(): port = int(port) break else: print('输入的端口错误') continue ip_port=(ip, port) bufsize=1024 sk = socket.socket(type=socket.SOCK_DGRAM) while True: msg='ntpdate' sk.sendto(msg.encode('utf-8'),ip_port) data=sk.recv(bufsize) print(data.decode('utf-8')) time.sleep(10) sk.close()
演示
# server 127.0.0.1 正在与你进行时间同步 127.0.0.1 正在与你进行时间同步 127.0.0.1 正在与你进行时间同步 # client 请输入时钟服务器的IP:127.0.0.1 请输入时间同步端口:9000 2018-06-09 14:45:19 2018-06-09 14:45:29 2018-06-09 14:45:39
#5-
#备注
4.3socket参数的详解
socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
family | 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。 |
type | 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。 |
proto | 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。 |
fileno | 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。 |
5.粘包
说明:同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是粘包。
1- 发送方的缓存机制
server端
import socket sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() conn,addr = sk.accept() data1 = conn.recv(15).decode('utf-8') data2 = conn.recv(15).decode('utf-8') print('--->1: ',data1) print('--->2: ',data2) conn.close() sk.close()
client端
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9000)) sk.send('hello'.encode('utf-8')) sk.send('alex'.encode('utf-8')) sk.close()
运行结果
# server --->1: helloalex --->2:
粘包只会发生在tcp协议中,在连续send的过程中就容易发生粘包
2- 接收方的缓存机制
server端
import socket sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() conn,addr = sk.accept() data1 = conn.recv(2).decode('utf-8') data2 = conn.recv(15).decode('utf-8') print('--->1: ',data1) print('--->2: ',data2) conn.close() sk.close()
client端
import socket sk = socket.socket() sk.connect(('127.0.0.1', 9000)) sk.send('hello'.encode('utf-8')) sk.send('alex'.encode('utf-8')) sk.close()
运行结果
# server --->1: he --->2: lloalex
总结:
粘包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
6.粘包的解决
原理:提前计算要发送数据的长度,相关模块struct
import struct ret = struct.pack('i', 111111) print(ret, len(ret)) # b'\x07\xb2\x01\x00' 4 # 说明:'i' 表示 int数据类型 # 返回一个4个字节的bytes字符串 # struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围 res = struct.unpack('i', ret) print(res) # (111111,) # 只有一个元素的元组
粘包问题的解决
server端
import socket import struct sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() conn,addr = sk.accept() inp = input('>>>> ').encode('utf-8') inp_len = len(inp) bytes_msg = struct.pack('i', inp_len) conn.send(bytes_msg) conn.send(inp) conn.close() sk.close()
client端
import socket import struct sk = socket.socket() sk.connect(('127.0.0.1', 9000)) msg_len = struct.unpack('i',sk.recv(4))[0] print(sk.recv(msg_len).decode('utf-8')) sk.close()
运行结果
# server >>>> 哈哈哈哈哈 # client 哈哈哈哈哈
7.socketserver并发网络编程
socket模块的局限性:使用TCP协议在同一时刻,server端只能与一个客户端通信
socket模块是底层模块,socketserver模块是基于socket模块添加功能封装的模块
参考:http://www.cnblogs.com/Eva-J/p/5081851.html -- 解读socketserver源码
server端
import time import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): conn = self.request print(conn) print(self.client_address) conn.send(b'hello') time.sleep(5) conn.send(b'hello2') myserver = socketserver.ThreadingTCPServer(('127.0.0.1',9000),Myserver) myserver.serve_forever()
client端
import socket import time sk = socket.socket() sk.connect(('127.0.0.1', 9000)) print(sk.recv(1024)) print(sk.recv(1024)) time.sleep(20) sk.close()
运行结果
# server <socket.socket fd=500, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 53135)> ('127.0.0.1', 53135) <socket.socket fd=516, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 53136)> ('127.0.0.1', 53136) <socket.socket fd=544, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 53137)> ('127.0.0.1', 53137) # client -- 可以同时开启多个客户端 b'hello' b'hello2' # client b'hello' b'hello2' # client b'hello' b'hello2'