客户端/服务器架构
C/S架构与socket的关系:
我们学习socket就是为了完成C/S架构的开发
osi七层:
应用层:
功能:提供用户接口
软件:QQ/微信、浏览器等各种能上网的应用程序
协议:HTTP、HTTPS、OICQ、Telnet、SSH等
提供用户接口,特指能够发起网络通信的应用程序。实际上,会话层,表示层、应用层也可以统称为应用层
问题:在现代软件开发中,如果我们编码还要根据不同编码方式进行代码实现,那么就不断进行重复劳动了。我们可以将表示层再进行包装吗?
解决方案:增加一层"应用层"。
表示层:
功能:担当数据的显示
使用何种编码方式。比如要传输的数据使用ASCII编码,视频还是二进制文件,是否要加密和压缩。发送端和接收端程序必须使用相同的编码方式,才能正确显示,否则就产生乱码。
问题:在现代软件开发中,如果我们编码还要根据不同编码方式进行代码实现,那么就不断进行重复劳动了。我们可以将表示层再进行包装吗?
解决方案:增加一层"应用层"。
会话层
功能:担当会话管理。
在两台电脑间,两个不同的应用程序间的:建立会话,区别于其他应用程序间的会话(如QQ的信息不会发送到浏览器中,使用端口号进行区分),保持会话,删除会话的过程。
问题:我们两台电脑间的通信,不仅仅是文字的交互。而是有多种数据格式的。那么会话层提供不了这个功能。
解决方案:增加一层"表示层"
传输层
功能:担当了可靠的端对端链接。
协议:TCP、UDP
提供了可靠或不可靠传输,能够纠正或失败重传,传输层通过端口号区分上层服务,并通过滑动窗口技术实现可靠传输、流量控制、拥塞控制等。传输层负责的是计算机之间的链接。
问题:尽管传输层提供了可靠的链接,那么当有一个发送方对多个接收方时,我们如何确定数据传输给哪一个接收方呢?又如何与其建立链接、保持链接、删除链接呢?
解决方案:增加一层"会话层"
网络层
功能:提供了三层寻址,三层数据转发功能
设备:路由器
协议:IP协议
路由器主要是根据IP地址来进行不同PC间的通信的。虽然路由器工作再网络层,但它实际上是兼有数据链路层、物理层的。所以在同一路由器下,IP地址、MAC地址不能相同,否则会发生冲突。当然路由器也存在一个IP地址,用于跟别的路由器进行通信,这样就可以屏蔽不同局域网协议间不能通信的问题了。
问题:仅仅通过路由器简单的发送数据可不行,如果因为网络的问题,导致数据丢失,数据传输不可控。这样就使得网络通信不可靠
解决方案:在网络层基础上,增加"传输层"
数据链路层
功能:提供了二层寻址、二层数据转发功能。
设备:网桥、交换机
协议:PPP、Ethernet、ARP、RARP..
数据链路层在不同的厂商有不同的实现,主要应用于没有路由器的情况下。多台电脑相互通信的情况,这种网络称为局域网。同一局域网中,MAC地址不能相同。
问题:不同协议间肯定是不能进行通信的。那么我们该如何使两个局域网之间进行通信呢?
解决方案:增加一层"网络层"
物理层
功能:提供物理规范,如线缆标准,接口标准
互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层
具体通信原理可看:
http://www.cnblogs.com/linhaifeng/articles/5937962.html
使用 socket一定要先学习互联网协议
C/S架构的软件(软件属于应用层)是基于网络进行通信的,网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。
socket层
socket是什么
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程
套接字工作流程
简单的socket实例:
服务端
__author__ = 'zyp' #-*- coding:utf-8 -*- import socket import os server = socket.socket() server.bind(('localhost',6969)) server.listen(5) while True: print("我在等电话") conn,addr=server.accept() print("电话来了") while True: data = conn.recv(1024) print('recv:',data.decode()) if not data : print("输入为空!") break res = os.popen(data.decode()).read() conn.send(res.encode()) server.close()
客户端
socket()模块函数用法
1 import socket 2 socket.socket(socket_family,socket_type,protocal=0) 3 socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。 4 5 获取tcp/ip套接字 6 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 7 8 获取udp/ip套接字 9 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 10 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',
我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。 11 例如tcpSock = socket(AF_INET, SOCK_STREAM)
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
基于TCP的套接字
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
tcp服务端
1 ss = socket() #创建服务器套接字 2 ss.bind() #把地址绑定到套接字 3 ss.listen() #监听链接 4 inf_loop: #服务器无限循环 5 cs = ss.accept() #接受客户端链接 6 comm_loop: #通讯循环 7 cs.recv()/cs.send() #对话(接收与发送) 8 cs.close() #关闭客户端套接字 9 ss.close() #关闭服务器套接字(可选)
tcp客户端
1 cs = socket() # 创建客户套接字 2 cs.connect() # 尝试连接服务器 3 comm_loop: # 通讯循环 4 cs.send()/cs.recv() # 对话(发送/接收) 5 cs.close() # 关闭客户套接字
如果在重启服务端时可能会遇到:地址被占用
这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址
解决方法:
#加入一条socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080))
基于UDP的套接字
udp是无链接的,先启动哪一端都不会报错
udp服务端
1 ss = socket() #创建一个服务器的套接字 2 ss.bind() #绑定服务器套接字 3 inf_loop: #服务器无限循环 4 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送) 5 ss.close() # 关闭服务器套接字
udp客户端
cs = socket() # 创建客户套接字 comm_loop: # 通讯循环 cs.sendto()/cs.recvfrom() # 对话(发送/接收) cs.close() # 关闭客户套接字
udp实例
# !usr/bin/env python # -*-coding:utf-8 -*- import socket server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) server.bind(("localhost",6666)) while True: conn,addr = server.recvfrom(1024) print(conn.decode(),addr) server.sendto(conn.upper(),addr)
# !usr/bin/env python # -*-coding:utf-8 -*- import socket client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg = input(">>:").strip() if not msg:continue client.sendto(msg.encode("utf-8"),("localhost",6666)) bak_msg,addr = client.recvfrom(1024) print(bak_msg.decode("utf-8"),addr)
粘包现象
基于tcp的socket,在运行时会发生粘包
基于udp的socket,在运行时永远不会发生粘包
注意注意注意:
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码
且只能从管道里读一次结果
注意:命令ls -l ; lllllll ; pwd 的结果是既有正确stdout结果,又有错误stderr结果
socket收发消息的原理
基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
两种情况下会发生粘包。
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数 据,产生粘包)
send(字节流)和recv(1024)及sendall
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失
解决粘包的方法
1.在连续发或者收中间再增加发和收。不建议使用
2.只收取相应文件大小
3.高级方法 struct模块
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据:http://www.cnblogs.com/linhaifeng/articles/6129246.html#_label12
需要补充实例
sockerserver
基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环
socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)
查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer
基于tcp的socketserver我们自己定义的类中的
- self.server即套接字对象
- self.request即一个链接
- self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的
- self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
- self.client_address即客户端地址
1.建立一个请求处理类,并继承这个类要继承BaseRequestHandler
2.重写父类的handle() #这里处理与客户端的交互
3.实例化TCPServer,并且传递server ip 和 你创建的请求处理类 给这个TCPServer
4. server.handle_request() #只处理一个请求
server.serve_forever() #处理多个一个请求,永远执行
server: import socketserver class MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): while True: try: self.data = self.request.recv(1024).strip() print("{} wrote".format(self.client_address[0])) print(self.data) self.request.send(self.data.upper()) except ConnectionResetError as e: print("error",e) break HOST,PORT = "localhost",9999 selver = socketserver.TCPServer((HOST,PORT),MyTCPHandler) selver.serve_forever() client: import socket client = socket.socket() client.connect(("localhost",9999)) while True: data = input(">>:").strip() if len(data) == 0: continue client.send(data.encode()) res = client.recv(1024).decode() print("res",res) client.close()
多并发
selver = socketserver.THreadingTCPServer((HOST,PORT),MyTCPHandler)