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()    #关机
server端
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()
client端

注意,传输过程中只能传输字节类型的数据。

#如果是英文和数字,直接使用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()
server端
import socket
sk = socket.socket()
ip_port = ('127.0.0.1', 8080)
sk.sendto(b'xx', ip_port)
sk.recvfrom(1024)
sk.close()
client端

基于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()
server端
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()
client端

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()
server端
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()
client端

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()
server端
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()
client端

第三部分、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()
服务端py
<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>
html

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()
服务端py
请输入:<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>
html

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()
服务端py
<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>
html
原文地址:https://www.cnblogs.com/yinwenjie/p/10856749.html