网络编程笔记

网络编程

软件开发架构

  • c/s架构

    c:客户端

    s:服务端

  • b/s架构

    b:浏览器

    s:服务端

手机端:好像C/S架构比较火,其实不然,微信小程序,支付宝第三方接口都类似于b/s架构

目的都在于统一接口,聚集用户群

pc端:b/s比较火

本质:b/s架构本质也是c/s架构

客户端与服务端概念

服务端:24小时不间断提供服务,谁来我就服务谁

客户端:想体验服务的时候,就去找服务端体验服务

学习网络编程能干什么

开发c/s架构的软件

学习并发,数据库,前端能干什么

开发b/s架构的软件

 

网络编程技术起源

绝大部分先进技术的兴起基本都来自于军事,网络编程这项技术就是来源于美国军事,为了实现数据的远程传输

 

人类实现远程沟通交流的方式

  • 插电话线的电话

  • 插网线的大屁股电脑

  • 插无线网卡的笔记本电脑

综上我们能够总结出第一个规律:要想实现远程通信第一个需要具备的条件就是:物理连接介质

再来想人与人之间交流,中国人说中文,外国人说外语,那如果想实现不同国家的人之间无障碍沟通交流是不是得规定一个大家都能听得懂的语言>>>英语

再回来看计算机领域,计算机之间要想实现远程通信除了需要有物理连接介质之外是不是也应该有一套公共的标准?这套标准就是>>>OSI七层协议(也叫OSI七层模型)

 

OSI七层协议(模型)

  • 应用层

  • 表示层

  • 会话层

  • 传输层

  • 网络层

  • 数据链路层

  • 物理连接层

也有人将其归纳为五层

  • 应用层

  • 传输层

  • 网络层

  • 数据链路层

  • 物理连接层

 

接下来我们就需要详细的看看每一层都有哪些需要我们了解掌握的知识点

1.物理连接层

​ 实现计算机之间物理连接,传输的数据都是01010的二进制​ 电信号工作原理:电只有高低电平

2.数据链路层("以太网协议")

​ 1.规定了二进制数据的分组方式​ 2.规定了只要是接入互联网的计算机,都必须有一块网卡!​ ps:网卡上面刻有世界唯一的编号:​ 每块网卡出厂时都被烧制上一个世界唯一的mac地址,​ 长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)​ 我们管网卡上刻有的编号叫电脑的>>>mac地址​ ----->上面的两个规定其实就是 "以太网协议"!

 

基于以太网协议通信:通信基本靠吼!!!弊端:广播风暴

交换机:如果没有交换机,你的电脑就变成了马蜂窝,有了交换机之后,所有的电脑只需要有一个网卡连接交换机,即可实现多台电脑之间物理连接

 

3.网络层(IP协议)

​ 规定了计算机都必须有一个ip地址​ ip地址特点:点分十进制​ 有两个版本ipv4和ipv6 为了能够兼容更多的计算机​ 最小:0.0.0.0 ​ 最大:255.255.255.255​ IP协议可以跨局域网传输

ip地址能够唯一标识互联网中独一无二的一台机器!

http://14.215.177.39/ 这个是百度搜索的Ip

注意,IP可以分为公网IP和局域网IP,公网IP是浏览器能直接访问到的IP,而局域网IP只有通过局域网内部才能访问到,或者先访问到这个局域网上面的公网IP,然后利用一些地址解释器等手段找到这个局域网IP,才能访问到这个主机

 

4.传输层(端口协议)

​ TCP,UDP基于端口工作的协议!​ 其实计算机之间通信其实是计算机上面的应用程序于应用之间的通信​ 端口(port):唯一标识一台计算机上某一个基于网络通信的应用程序​ 端口范围:0~65535(动态分配)​ 注意:0~1024通常是归操作系统分配的端口号​ 通常情况下,我们写的软件端口号建议起在8000之后​ flask框架默认端口5000​ django框架默认端口8000​ mysql数据库默认端口3306​ redis数据库默认端口6379注意:一台计算机上同一时间一个端口号只能被一个应用程序占用

 

小总结: IP地址:唯一标识全世界接入互联网的独一无二的机器 port端口号:唯一标识一台计算机上的某一个应用程序 ip+port :能够唯一标识全世界上独一无二的一台计算机上的某一个应用程序

补充: arp协议:根据ip地址解析mac地址

 

5.应用层(HTTP协议,FTP协议)

 

6.TCP协议(流式协议,可靠协议)

三次握手四次挥手

  • 三次握手建连接

  • 四次挥手断连接

 

补充:星轨:明星出轨

 

Socket(套接字编程)

socket简介:https://www.cnblogs.com/Dominic-Ji/articles/10929376.html

类似于操作系统将复杂丑陋的控制计算机硬件的操作封装成统一简单的接口,只需要使用者学会如何操作操作系统就可以简单快速的操作计算机硬件

# 服务端
import socket


s = socket.socket()  # 买手机
s.bind(('127.0.0.1',8080))  # 插电话卡 bind(('ip','port'))
s.listen(5)  # 开机
conn,addr = s.accept()  # 接听电话 conn是链接对象(电话) addr是对方的地址
data = conn.recv(1024)  # 听别人说话
print(data)
conn.send(b'hello baby!')  # 我和别人说话,网络传输数据必须是bytes类型(0101的二进制)

conn.close()  # 挂电话
s.close()  # 关机

# 客户端
import socket


c = socket.socket()  # 买手机
c.connect(('127.0.0.1',8080))  # 拨号


c.send(b'hello big big baby!')
data = c.recv(1024)
print(data)

c.close()

通信循环


# 客户端不能发空
# 针对linux和mac服务端在客户端断开链接后或反复收空
# 异常捕获 增加程序健壮性

链接循环


"""
服务端三大特点:
要有固定的ip和port
24小时不间断提供服务
支持高并发
"""
# 对于报错消息是Address already in use
from socket import SOL_SOCKET,SO_REUSEADDR

s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)  # 在bind之前设置,避免服务重启时避免报Address already in use

# 对于无法对同一个文件运行多个窗口的情况,需要手动设置allow parallel run

半链接池概念

"""
多个客户端同时链接服务端
"""

 

TCP粘包问题

1.subprocess模块运用


import subprocess
res = subprocess.Popen('dir',shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
print('stdout:',res.stdout.read().decode('gbk'))  # windows操作系统默认gbk编码
print('stderr:',res.stderr.read().decode('gbk'))

2.基于TCP+subprocess实现远程执行用户命令


# 服务端
import socket
import subprocess

server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)

conn, addr = server.accept()
while True:
   cmd = conn.recv(1024).decode('utf-8')
   res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   stdout = res.stdout.read()
   stderr = res.stderr.read()
   data = stdout + stderr
   print(len(data))
   conn.send(data)
conn.close()
server.close()

# 客户端
import socket

client = socket.socket()
client.connect(('127.0.0.1', 8080))

while True:
   cmd = input('>>>:').encode('utf-8')
   client.send(cmd)
   data = client.recv(1024)
   print(data.decode('utf-8'))
client.close()

客户端先输入tasklist,再输入dir发现服务端两次收到的结果都是tasklist命令的结果

粘包问题再介绍


send(b'hello')
send(b'world')

recv(1024)  # 一次收完
recv(1024)  # 收到为空

# 基本解决思路
send(b'hello')  >>> recv(5)
send(b'world')  >>> recv(5)
send(b'baby')  >>> recv(4)

"""
在发送数据时,需要能够提前告诉接收端,我马上要发给你的数据长度是多少,你后面再对应收就行
但是问题又来了,我这个提示性的信息(报头),报头的长度我们也不知道,我们能怎么做?是不是应该规定一个准则,报头的长度必须是固定的并且报头里面能够携带真实数据的相关信息
所以现在只需要解决一件事>>>:报头长度固定且能够携带真实数据的相关信息
"""

3.struct模块


import struct
obj = struct.pack('i',123456)
print(len(obj))  # 4
obj = struct.pack('i',898898789)
print(len(obj))  # 4
# 无论数字多大,打包后长度恒为4

obj1 = struct.unpack('i',obj)
print(obj1)  # 解析后,获取对应的真实数据长度

4.解决粘包问题简单版本


# 服务端
from socket import *
import subprocess
import struct
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8081))
server.listen(5)
# 链接循环
while True:
   conn, client_addr = server.accept()
   print(client_addr)
   # 通信循环
   while True:
       try:
           cmd = conn.recv(1024) #cmd=b'dir'
           if len(cmd) == 0: break  # 针对linux系统
           obj=subprocess.Popen(cmd.decode('utf-8'),
                            shell=True,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE
                            )
           stdout=obj.stdout.read()
           stderr=obj.stderr.read()
           # 1. 先制作固定长度的报头
           header=struct.pack('i',len(stdout) + len(stderr))
           # 2. 再发送报头
           conn.send(header)
           # 3. 最后发送真实的数据
           conn.send(stdout)
           conn.send(stderr)
       except ConnectionResetError:
           break
   conn.close()
server.close()

# 客户端
from socket import *
import struct
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8081))
# 通信循环
while True:
   cmd=input('>>: ').strip()
   if len(cmd) == 0:continue
   client.send(cmd.encode('utf-8'))
   #1. 先收报头,从报头里解出数据的长度
   header=client.recv(4)
   total_size=struct.unpack('i',header)[0]
   #2. 接收真正的数据
   cmd_res=b''
   recv_size=0
   while recv_size < total_size:
       data=client.recv(1024)
       recv_size+=len(data)
       cmd_res+=data
   print(cmd_res.decode('gbk'))
client.close()

5.解决粘包问题终极版本

如果我要传输的是文件数据,接收方不仅仅需要知道文件数据的大小,还想知道文件数据的文件名,文件描述,文件md5值等信息,这个时候改如何解决


import struct
import json
header_dic={
               'filename':'a.txt',
               'md5':'asdfasdf123123x1',
               'total_size':123123111111111111
          }
header_json=json.dumps(header_dic)
# print(header_json)
header_bytes=header_json.encode('utf-8')
print(len(header_bytes))  # json序列化过后的数据长度仅仅只有几百大小。刚好帮我们解决了strcut打包格式选择的问题

# 服务端
import socket
import json
import struct

server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)

while True:
   conn, addr = server.accept()
   while True:
       try:
           header = conn.recv(4)
           if len(header) == 0:break
           dic_len = struct.unpack('i', header)[0]

           real_dic = json.loads(conn.recv(dic_len).decode('utf-8'))
           print(real_dic)
           file_name = real_dic.get('file_name')
           file_size = real_dic.get('file_size')
           recv_size = 0
           with open(file_name, 'wb') as f:
               while recv_size < file_size:
                   recv_data = conn.recv(1024)
                   f.write(recv_data)
                   recv_size += len(recv_data)
       except ConnectionResetError:
           break

# 客户端
import struct
import json
import socket
import os

client = socket.socket()
client.connect(('127.0.0.1', 8080))

file_size = os.path.getsize(r'/Users/jiboyuan/PycharmProjects/aboutsocket/10 解决粘包问题终极版.mp4')
file_path = r'/Users/jiboyuan/PycharmProjects/aboutsocket/10 解决粘包问题终极版.mp4'
data_dic = {
   'file_name': '澳门最大线上赌场开业啦.mp4',
   'file_size': file_size
}
header_json = json.dumps(data_dic)
header_bytes = header_json.encode('utf-8')
# 制作字典的报头
header = struct.pack('i', len(header_bytes))
# 发送报头
client.send(header)
# 发字典数据
client.send(header_bytes)
# 打开文件发送文件数据
with open(file_path,'rb') as f:
   for line in f:
       client.send(line)

整体思路:先发字典的报头,再发字典数据,最后发真实数据

 

UDP协议

数据报协议


"""
1.udp协议客户端允许发空
2.udp协议不会粘包
3.udp协议服务端不存在的情况下,客户端照样不会报错
"""

# 服务端
import socket

server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))

msg, addr = server.recvfrom(1024)
print(msg.decode('utf-8'))
server.sendto(b'hello', addr)

server.close()

#客户端
import socket

client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1', 8080)

client.sendto(b'hello server baby!', server_addr)
msg, addr = client.recvfrom(1024)
print(msg, addr)

"""
# udp特点 >>> 无链接,类似于发短信,发了就行对方爱回不回,没有任何关系
# 将服务端关了,客户端起起来照样能够发数据。因为不需要考虑服务端能不能收到
"""
# 验证udp协议有无粘包问题
import socket
server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
print(server.recvfrom(1024))
print(server.recvfrom(1024))
print(server.recvfrom(1024))

import socket
client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1',8080)
client.sendto(b'hello',server_addr)
client.sendto(b'hello',server_addr)
client.sendto(b'hello',server_addr)

基于UDP实现简易版本的qq


# 服务端
import socket

server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))
while True:
   msg, addr = server.recvfrom(1024)
   print(addr)
   print(msg.decode('utf-8'))
   info = input('>>>:').encode('utf-8')
   server.sendto(info, addr)

server.close()

# 多个客户端
import socket

client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1', 8080)

while True:
   info = input('>>>:')
   info = ('来自客户端1的消息:%s'%info).encode('utf-8')  # 改中文备注即可
   client.sendto(info, server_addr)
   msg, addr = client.recvfrom(1024)
   print(msg.decode('utf-8'), addr)

client.close()

小知识点补充:

windows电脑和mac电脑的时间同步功能,其实就是基于udp朝windows,mac服务器发送请求获取标准时间

总结:

​ TCP协议就类似于打电话

​ UDP协议就类似于发短信

SocketServer模块介绍(让tcp也能支持并发)


# TCP socketserver使用
import socketserver
class MyTcpServer(socketserver.BaseRequestHandler):
   def handle(self):
       while True:
           try:
               data = self.request.recv(1024)  # 对于tcp,self.request相当于conn对象
               if len(data) == 0:break
               print(data)
               self.request.send(data.upper())
           except ConnectionResetError:
               break
if __name__ == '__main__':
   server = socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyTcpServer)
   server.serve_forever()

   
# UDP socketserver使用
import socketserver


class MyUdpServer(socketserver.BaseRequestHandler):
   def handle(self):
       while True:
           data, sock = self.request
           print(data)
           sock.sendto(data.upper(), self.client_address)


if __name__ == '__main__':
   server = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyUdpServer)
   server.serve_forever()

 

上一篇:Python:模拟进度条


下一篇:从IO模型到协程(二) BIO模型和NIO模型