Python 17 socket网络编程
网络编程
第一部分:计算机网络基础
一、软件开发架构
1、应用类——C/S架构(client / server)
服务端一直运行,等待服务别人。客户端寻求服务。
客户端通过互联网与服务端建立连接
2、web类——B/S架构(broser / server)
b/s其实也是一种c/s架构,只是将客户端都变成了浏览器或某个程序,统一连接的入口。
二、网络基本概念
1、mac地址
计算机通过网卡和网线与其他机器进行连接,每一台计算机都有唯一的网卡编号,称之为mac地址。但是mac地址很长很复杂,不容易记忆,于是人们将mac地址按照一定的规则重新改编成由4个点分十进制组成的IP地址。
2、IP地址
由4个点分十进制数组成,每个数的范围是0~255,其本质是一个8位二进制数。
3、arp协议
规定了如何通过ip地址直到mac地址的协议。发送方将封装了目标IP、自身IP和mac地址的数据传输给交换机,交换机通过广播的方式发送给目标网段,目标收到后确认是自己的IP,会将自己的mac返回给交换机,交换机将双方的IP和mac缓存,以后就可以知道IP和mac的对应关系。
4、端口
相当于是一台机器上的一个应用程序的编号,同一时期同一机器上,一个端口号只能由一个应用程序占用。范围是0--65535。
5、交换机
用来解决多台机器间的通信问题。当机器a需要给机器b发送消息时,通过广播的方式向局域网内的所有机器发送讯息,b接受到消息确认是自己时通过单播的方式回复a。arp协议就是a拿着b的ip地址广播,b确定是自己的ip后将自己的mac地址返回给a,a将这个mac地址缓存下来,后面再进行通信就直接能定位到b,不再需要广播。
6、路由器
路由器将多个交换机连接起来。机器不能直接和局域网外的机器通信,需要通过网关。通过IP地址和子网掩码按位与运算,得到局域网的网段。
7、TCP协议
特点:可靠的、面向连接、无边界字节流、速度较慢、占用操作系统的链接资源。
①可靠:TCP协议会保证数据的完整性,每次发送消息都需要受到一条回执,否则会重复发送,确保对方收到。不会发生丢包。
②面向连接:必须先建立连接再通信,实现一个全双工的双向通信。全双工意思是客户端和服务端可以互相收发消息。这个过程就像是打电话,双方必须一直保持电话才可以通信。
③无边界字节流:多条数据之间是没有边界的,无法区分,会产生粘包,只能在应用层解决。
④速度慢:因为面向连接且可靠,建立连接和断开连接都需要时间,并且对于数据完整性的保护也需要消耗时间。
⑤占用操作系统资源:在不使用任何异步机制、在阻塞IO模型下,一个server端只能和一个client端连接
通过tcp协议建立链接需要通过三次握手和四次挥手:
三次握手:客户端向服务端发送SYN请求建立链接——服务端同意(ACK)并请求向客户端建立链接(SYN)——客户端同意(ACK)。
四次挥手:客户端向服务端请求断开连接——服务端同意, 服务端向客户端请求断开连接——客户端同意。
为什么是三次握手和四次挥手呢?因为TCP协议是全双工的,建立在双方通信的基础上,所以如果服务端同意客户端向自己建立连接,那么一定会请求与客户端建立连接,同意客户端的请求和发送自己的请求是绑定在一起必须一起发生的,这两个步骤可以合并。但是断开连接的过程并不是,客户端可以单方面的断开与服务端的连接,而服务端可以选择继续保持连接,因此同意断开和请求断开并不是一定绑定在一起的。
8、UDP协议
UDP协议是不可靠的、无连接、面向数据报、速度快、能够传输的数据长度有限、可以和任意多个客户端通信。他不建立连接,直接将信息传递到网络中。这个过程就像是发信息,不用管对方是否看到,都可以直接发送数据。
9、http协议
超文本传输协议,用于服务端和浏览器进行数据交互,规定了请求和响应的格式,底层基于TCP协议,可以传输任意类型的数据。
工作流程一般是客户端与服务器建立连接——客户端向服务器发送请求——服务器接收请求并根据请求返回数据——断开连接。http请求是一种无状态的短连接,为了应对数量庞大的网络请求,所以http连接都是一次性的,每次只处理一个请求,请求结束则断开,这样有限的请求数才能更上需求,且服务器不会保存每次请求的状态,不会保留信息,大大减轻了存储负担。
http请求分为三个部分:
- 状态行:请求方式、url和协议版本
- 请求头:访问的域名、用户代理、cookie等信息
- 请求体:请求的数据
http响应也分为三个部分:
- 状态行:状态码、响应短语、协议版本
- 响应头:搭建服务器的软件、发送响应的时间、相应数据的格式等
- 响应体:具体数据
HTTP请求报文格式:
HTTP响应报文格式:
10、https协议
http协议是不安全的,体现在三个方面:内容不加密可能泄露、无法认证双方的身份、无法确认收到的内容是否完整。
https是http+ssl(secure socket layer安全套接层)构成的协议,用来解决http的安全问题,其实质就是在http协议后,传输层之前添加一层加密。https使用公开密钥加密和共享密钥加密相结合的混合密钥加密方式,具体流程如下:建立连接时,服务端将封装了自己密钥的证书发送给客户端,客户端使用认证机构的公钥解密,得到服务端的公钥,之后的通信过程中,客户端和服务端都通过这一共享公钥进行加密解密。
三、互联网协议——osi五层模型
互联网协议就是只当进行网络通信时,数据的传输格式的规范。
分为应用层、传输层、网络层、数据链路层和物理层。
应用层包裹了真正需要传输的数据,传输层包装了使用的传输协议比如TCP、UDP,网络层包装了目标的IP地址,数据链路层包装了发送者的mac地址,物理层将这些数据都转换成01这样的电信号。
第二部分:socket学习
一、socket基本使用
socket又叫套接字,是python提供的用来帮助我们将数据包装成符合互联网协议格式的数据。
1、基于TCP协议的socket
import socket sk = socket.socket() #创建socket对象,买手机 sk.bind(('127.0.0.1', 8080)) #绑定ip和端口,绑定手机卡 sk.listen() #开始监听,打开手机等待电话 conn, addr = sk.accept() #接到电话,在这里已经经过了三次挥手建立了连接,程序会阻塞 ret = conn.recv(1024).decode('utf-8') #听到别人说的话 conn.send(b'asd') #对别人说话 conn.close() #挂断电话 sk.close() #关机
import socket sk = socket.socket() sk.connect(('127.0.0.1', 8080)) sk.send(b'xxxx') ret = sk.recv(1024).decode('utf-8') sk.close()
注意,传输过程中只能传输字节类型的数据。
#如果是英文和数字,直接使用b'' data = b'xxxxx' #如果是中文字符,两种方式编码 data = '你好'.encode('utf-8') data = bytes('你好', encoding='utf-8') #解码 ret.decode('utf-8')
2、基于UDP协议的socket
import socket sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind(('127.0.0.1', 8080)) msg, addr = sk.recvfrom(1024) sk.sendto(b'xxx') sk.close()
import socket sk = socket.socket() ip_port = ('127.0.0.1', 8080) sk.sendto(b'xx', ip_port) sk.recvfrom(1024) sk.close()
基于UDP的socket服务端不需要监听,也不能主动发消息,只能被动地等待客户端发送消息,获取客户端发送的信息和其地址,回复的时候也需要带上客户端地址。
二、黏包现象
现象:当发送消息时,接收方得到的消息混乱,和上一次没接收完的消息混在一起。
只有TCP协议才会有黏包现象,UDP协议没有。TCP协议会将超出限制的部分在下一次的数据传输中获取,连接非常可靠,不会丢包,但是也导致消息没有边界,出现黏包问题。而UDP协议直接将超出的部分丢弃,不存在黏包问题,一次只接收一次的数据,但是会导致丢包。
产生原因:
服务端:1、TCP是长连接,每一次发送数据服务端都会进行一次确认,发送次数越多网络延迟越大,所以TCP有自己的优化算法,如果一次发送的数据非常小,会先进行缓存,在很短的时间内如果又发送数据,会将两次数据的包合并,一起发送,即连续多次send,这样减少了发送次数。2、当一次发送的数据太长超过其限制时,服务端会自己将其拆包发送,等于是发送了多次。
客户端:1、客户端收到的数据都会放在缓存中,等待接收,如果第一次的数据还没有接收就又有数据发来,则多次数据粘在一起。2、如果一次的数据过长,超出接收的长度,TCP会在服务端将超出的部分缓存,等待下一次接收数据时再一起获取。
黏包的解决方式:
1、简陋的方式
黏包的本质原因就是服务端不知道一次消息的长度是多少,导致不知道每次到底该接受多少,所以可以在发送数据前,先把数据的长度发送给服务端,服务端再接收固定长度的数据就可以避免黏包。
import socket sk = socket.socket() sk.bind(('127.0.0.1', 8081)) sk.listen() conn, addr = sk.accept() while True: cmd = input('>>>').encode('utf-8') conn.send(cmd) data_len = conn.recv(1024).decode('utf-8') conn.send(b'ok') data = conn.recv(int(data_len)).decode('gbk') print(data) conn.close() sk.close()
import socket import subprocess sk = socket.socket() sk.connect(('127.0.0.1', 8081)) while True: cmd = sk.recv(1024).decode('utf-8') ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = ret.stdout.read() stderr = ret.stderr.read() ret_len = len(stdout) + len(stderr) sk.send(str(ret_len).encode('utf-8')) ret = sk.recv(1024) sk.send(stdout) sk.send(stderr) sk.close()
2、改进的方式
方式一有一个缺陷,当发送数据长度时,服务端并不知道这个长度数据是多长,所以也只能定为接收1024个字节,然后还需要返回给客户端一个值,客户端接收后再发送数据,不然客户端还是连着发几个数据,一样会造成黏包,但是这样就多了一次网络延迟,性能较差。所以针对这种情况,可以使用struct模块,他可以帮助我们将任意长度的数字转换成固定长度的字节,这样第一次发送的长度数据是一个固定的值,服务端也接收固定的值,客户端就可以连着发送后面的数据,不需要双方进行确认来延迟了。
import struct import socket sk = socket.socket() sk.bind(('127.0.0.1', 8081)) sk.listen() conn, addr = sk.accept() while True: cmd = input('>>>').encode('utf-8') conn.send(cmd) data_len = struct.unpack('i',conn.recv(1024))[0] data = conn.recv(data_len).decode('gbk') print(data) conn.close() sk.close()
import struct import socket import subprocess sk = socket.socket() sk.connect(('127.0.0.1', 8081)) while True: cmd = sk.recv(1024).decode('utf-8') ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = ret.stdout.read() stderr = ret.stderr.read() ret_len = len(stdout) + len(stderr) sk.send(struct.pack('i', ret_len)) sk.send(stdout) sk.send(stderr) sk.close()
3、完善的方式
在网络上传输的所有数据都叫做数据包,数据包里的数据都叫做报文,报文里除了数据还有IP、mac、端口号等,所有的报文都有报头,报头告诉另一方报文的长度是多少。我们可以自己定制报文和报头,将数据的长度等信息放在报头中。
三、socketserver
socket只能实现服务器在同一时刻与一个客户端进行通讯,socketserver提供了并发实现socket服务器的功能。
import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): ret = self.request.recv(1024).decode('utf-8') #self.request就相当于是conn print(ret) self.request.send(b'hi') if __name__ == "__main__": socket = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyServer) socket.serve_forever()
import socket sk = socket.socket() sk.connect(('127.0.0.1', 8080)) sk.send(b'hello') ret = sk.recv(1024).decode('utf-8') print(ret) sk.close()
第三部分、websocket
一、简介
1、轮询
客户端不断向服务端发送请求,服务端也不断的询问并回复,这种方式占用的资源太多,但是能保证数据实时性。
2、长轮询
客户端向服务端发送请求,服务端接收后在一段时间内去询问,如果有消息就返回,如果超时客户端再向服务端发起一次请求。这种方式节省了一定的资源浪费,但是数据实时性相对不好。
3、websocket
客户端与服务端建立长连接,不再断开,可以互相发送消息。
建立websocket连接就是在基本的http请求中加入一些请求头,升级成websocket协议,之后收发的消息都会有加密解密的过程。
二、websocket使用
1、简单使用
import json from flask import Flask, request, render_template from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer from geventwebsocket.websocket import WebSocket app = Flask(__name__) user_socket_dict = {} @app.route("/ws") def ws(): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket print(user_socket) while 1: msg = user_socket.receive() print(json.loads(msg)) user_socket.send('hello') @app.route("/") def index(): return render_template("websocket.html") if __name__ == "__main__": http_serv = WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler) http_serv.serve_forever()
<script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:5000/ws"); ws.onmessage = function(data){ console.log(data); console.log(data.data); } </script>
2、群聊
import json from flask import Flask, request, render_template from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer from geventwebsocket.websocket import WebSocket app = Flask(__name__) user_socket_list = [] @app.route("/ws") def ws(): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: user_socket_list.append(user_socket) while True: msg = user_socket.receive() for sk in user_socket_list: if sk == user_socket: continue try: sk.send(msg) except: user_socket_list.remove(sk) @app.route("/") def index(): return render_template("群聊.html") if __name__ == "__main__": http_serv = WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler) http_serv.serve_forever()
请输入:<input type="text" id="msg"><button onclick="send_msg()">发送</button> <div id="msg_list"></div> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:5000/ws"); ws.onmessage = function(data){ var tag = document.createElement("div"); tag.innerText = data.data; document.getElementById("msg_list").appendChild(tag); }; function send_msg(){ var msg = document.getElementById("msg").value; var tag = document.createElement("div"); tag.innerText = msg; document.getElementById("msg_list").appendChild(tag); document.getElementById("msg").value = ""; ws.send(msg) } </script>
3、单聊
import json from flask import Flask, request, render_template from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer from geventwebsocket.websocket import WebSocket app = Flask(__name__) user_socket_dict = {} @app.route("/ws/<username>") def ws(username): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: user_socket_dict[username] = user_socket while True: msg = user_socket.receive() # {"from_user": "xxx", "to_user": "xxx", "msg": "xxx"} msg_dict = json.loads(msg) to_socket = user_socket_dict.get(msg_dict.get("to_user")) to_socket.send(msg) @app.route("/") def index(): return render_template("单聊.html") if __name__ == "__main__": http_serv = WSGIServer(('127.0.0.1', 5000), app, handler_class=WebSocketHandler) http_serv.serve_forever()
<div>我:<input id="from_user"><button onclick="connect()">建立连接</button></div> <div>发送给:<input id="to_user"></div> <div>消息:<input id="msg"><button onclick="send_msg()">发送</button></div> <div id="msg_list"></div> <script type="text/javascript"> var ws = null; function connect(){ var from_user = document.getElementById("from_user").value; ws = new WebSocket("ws://127.0.0.1:5000/ws/" + from_user); ws.onmessage = function(data){ var msg_obj = JSON.parse(data.data); var tag = document.createElement("div") tag.innerText = msg_obj.from_user + ":" + msg_obj.msg; document.getElementById("msg_list").appendChild(tag); } } function send_msg(){ var from_user = document.getElementById("from_user").value; var to_user = document.getElementById("to_user").value; var msg = document.getElementById("msg").value; var msg_obj = {from_user:from_user, to_user:to_user, msg:msg}; var tag = document.createElement("div"); tag.innerText = "我:" + msg; document.getElementById("msg_list").appendChild(tag); ws.send(JSON.stringify(msg_obj)); } </script>