2

网络

网络通信

网络: 一种辅助双方或者多方能够连接在一起的工具

网络的目的:就是为了联通多方然后进行通信,即把数据从一方传递给另外一方。

使用网络能够把多方链接在一起,然后可以进行数据传递
网络编程: 让在不同的电脑上的软件能够进行数据传递,即进程之间的通信

TCP/IP协议

TCP/IP协议(协议族)

为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议。
互联网协议簇(Internet Protocol Suite)就是通用协议标准。

因为互联网协议包含了上百种协议标准,但是最重要的两个协议是TCPIP协议,所以把互联网协议简称为:TCP/IP协议

四层分类:链路层 -> 网络层 -> 传输层 -> 应用层
七层分类:物理层 -> 数据链路层 -> 网络层 -> 传输层 -> 会话层 -> 表示层 -> 应用层

clipboard.png

网际层也称为:网络层
网络接口层也称为:链路层

常用的:TCP(模型:电话), UDP(模型:写信), ARP, IP

应用层:解决要传递什么数据
传输层:解决如何传递数据udp, tcp
网络层:解决地理位置坐标
链路层:具体的传输工具

端口

端口:

  • 操作系统为了区分数据给哪个进程,增加了一个标识端口
  • 进出进程的通道

Pid端口: 同一台操作系统中,pid一定不同,而且可知,但是多台操作系统中,Pid不一定能够唯一, 不能够知道其它操作系统的进程的pid;而端口在多台操作系统中是唯一的。

端口作用:为了区分多个操作系统下具体是哪个进程。

端口号: 端口是通过端口号来标记的,端口号只有整数,范围是从0到65535(在Linux系统中,端口可以有65536(2的16次方)个。)

端口分类:

  • 知名端口
    知名端口是众所周知的端口号,范围从0到1023: 80端口分配给HTTP服务。21端口分配给FTP服务。22端口分配给SSH服务
  • 动态端口
    动态端口的范围是从1024到65535。
    它一般不固定分配某种服务,而是动态分配。
    动态分配是指当一个系统进程或应用程序进程需要网络通信时,它向主机申请一个端口,主机从可用的端口号中分配一个供它使用。
    当这个进程关闭时,同时也就释放了所占用的端口号。

查看端口: netstat -an

IP地址

地址: 用来标记地点的

IP地址的作用: 用来在网络中标记一台电脑的一串数字,比如192.168.1.1,在本地局域网上是唯一的。

IP地址的分类:A类B类C类D类E类

每一个IP地址包括两部分:网络地址主机地址

clipboard.png

A类IP地址
第一个字节不变(网络号不变),其它字节可以变化(主机号可变)。
一个A类IP地址:1字节的网络地址 + 3字节主机地址组成, 网络地址的最高位必须是“0”

地址范围:1.0.0.1 - 126.255.255.254
二进制表示为:00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110
A类的可用网络有126个,每个网络能容纳1677214个主机

B类IP地址
第一个字节和第二个字节不变(网络号不变),其它字节可以变化(主机号可变)。
一个B类IP地址:2个字节的网络地址 + 2个字节的主机地址组成,网络地址的最高位必须是“10”

地址范围:128.1.0.1 - 191.255.255.254
二进制表示为:10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110
可用的B类网络有16384个,每个网络能容纳65534主机

C类IP地址
第一个字节和第二个字节不变,第三个字节(网络号不变),其它字节可以变化(主机号可变)
一个C类IP地址:3个字节的网络地址 + 1个字节的主机地址组成,网络地址最高为必须是“110”

地址范围:192.0.1.1 - 233.255.255.254
二进制表示为:11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110
C类网络可达2097152个,每个网络能容纳254(2^8 - 2[0,255不能使用])个主机

D类地址
D类地址用于多点广播

D类IP地址第一个字节以1110开始,它是一个专门保留的地址。
它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中
多点广播地址用来一次寻址一组计算机

地址范围:224.0.0.1 - 239.255.255.254

E类IP地址
1111开始,为将来使用保留
E类地址保留,仅作实验和开发用

私有IP

国际规定有一部分IP地址是在局域网中使用,属于私网IP,不在公网中使用

10.0.0.0 - 10.255.255.255
172.16.0.0 - 172.31.255.255
192.168.0.0 - 192.168.255.255

Note:
相同网段:192.168.1前面3个字节相同就称之相同网段,即网络号不变就是相同网段

IP地址范围:127.0.0.1 - 127.255.255.255用于回路测试

192.168.1.0 => 192.168.1.00000000: 网络号
192.168.1.255 => 192.168.1.11111111: 广播号

socket

socket套接字作用:多台电脑间进程的通信

socket.socket(AddressFamily, Type)

Address Family: 可以选择AF_INET(用于Internet进程间通信)或者AF_UNIX(用于同一台机器进程间通信),一般使用AF_INET
Type:套接字类型,可以是SOCK_STREAM(流式套接字,主要用于TCP协议);也可以是SOCK_DGRAM(数据报套接字,主要用于UDP协议)

TCP通信:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

UDP通信:

import socket
s = socket.socket(socket.AF_INIT, socket.SOCK_DGRAM)

TCP通信好处:不会丢失数据,数据可靠。
TCP通信坏处:速度慢。

UDP

UDP用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。UDP不提供可靠性,它只把应用程序传递IP曾的数据报发出去,但是并不能保证数据达到目的地。
由于UDP在传输数据报前不用在客户和服务端之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

UDP是一种面向无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此是否能达到目的地,到达目的地的地址以及内容的正确性都是不能保证的。

UDP特点:

  • UDP是面向无连接的通讯协议,UDP数据报括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。
  • UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内。
  • UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。

UDP一般用于多点通信实时的数据业务:

  • 语音广播
  • 视频
  • QQ
  • TFTP(简单文件传送)
  • SNMP(简单网络管理协议)
  • RIP(路由信息协议,如报告股票市场,航空信息)
  • DNS(域名解释)
UDP发送数据
  1. 创建客户端套接字
  2. 发送/接收数据
  3. 关闭套接字
from socket import *


udpSocekt = socket(AF_INET, SOCK_DGRAM)

udpSocket.send(b'msg', '192.168.1.201', 8081)

udpSocket.close()
UDP发送、接收数据
# coding=utf-8

from socket import *


udpSocket = socket(AF_INET, SOCK_DGRAM)
sendAddr = ('192.168.1.201', 8080)
sendData = input("请输入要发送的数据:")
udpSocket.sendto(sendData, sendAddr)

recvData = udpSocket.recvfrom(1024) # 1024表示本次接收的最大字节数

print(recvData)

# 关闭套接字
udpSocket.close()
端口问题

运行程序端口号会变化:
每重新运行一次网络程序,端口不一样的原因:数字标识这个网络程序,当重新运行时,如果没有确定到底用哪个,系统默认会随机分配。
这个网络程序在运行的过程中,这个就唯一标识这个程序,所以如果其它电脑上的网络程序如果想要向此程序发送数据,那么就需要向这个数字(即端口)标识的程序发送。

在同一个OS中,端口不允许相同,即如果某个端口已经被使用了,那么在这个端口进程释放该端口之前,其它进程不能使用该端口。

绑定信息

一般情况下,一天电脑上运行的网络程序有很多,而各自用端口号很多情况下,为了不育其它的网络程序占用同一个端口好,往往在编程中,udp的端口号一般不绑定。

bind作用:固定端口和IP

socket接收信息:

# coding=utf-8

from socket import *


udpSocket = socket(AF_INET, SOCK_DGRAM)
bindAddr = ('', 9001) # ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udpSocket.bind(bindAddr) # 绑定ip和端口号

recvData = udpSocket.recvfrom(1024) # 1024表示本次接收的最大字节数

udpSocket.close()

一个udp网络程序,可以不绑定,此时操作系统会随机进行分配一个端口,如果重新运行次程序端口可能会发生变化
一个udp网络程序,也可以绑定信息(ip地址,端口号),如果绑定成功,那么操作系统用这个端口号来进行区别收到的网络数据是否是此进程的

发送方不需要绑定信息,服务方(接收方)需要绑定信息。
接收和发送,可以同时进行。

网络通信中的工作方式:

  • 单工:只能往一个方向发送数据。例如(收音机)
  • 半双工:一方在传输,另一方无法进行其它操作,同一时刻,只能执行一方。例如(对讲机)
  • 全双工: 二个方向可以同时传输数据。例如(电话)

网络套接字(UDP和TCP)是全双工,例如(下载的同时,可以同时上传)

Python3编码问题

socket.sendto()Python3默认需要字节一样的对象。不能使用str类型传递。

发送数据时的解决方法:

sendData = 'msg'
# udpSocket.sendto(sendData.encode('utf-8'), (ip, prot))
udpSocket.sendto(sendData.encode('gb2312'), (ip, prot))

接收数据时的解决方法:

recvData =udpSocket.recvfrom(1024)

content, destInfo = recvData

print('content is %s'%content.decode('gb2312')) # decode() 默认是utf-8
UDP网络通信过程

clipboard.png

echo服务器

echo(回显)服务器: 发送一条数据,返回一条数据。

# coding=utf-8

from socket import *


udpSocket = socket(AF_INET, SOCK_DGRAM)

bindAddr = ('', 9001)
udpSocket.bind(bindAddr)

num = 1
while True:
    recvData = udpSocket.recvfrom(1024)
    udpSocket.sendto(recvData[0], recvData[1]) # 将接收到的数据再发送给对方
    print('已经将接收到的第%d个数据返回给对方,内容为:%s'%(num, recvData[0]))
    num += 1

udpSocket.close()
udp广播

TCP没有广播,即广播只能在UDP中使用,TCP使用不了。

信息发送到设备上(交换机),该设备再处理信息到各个目的地。

网络通信中的几种通讯模式:

  • 单播: 点对点传输。例如:QQ聊天信息中的个人聊天
  • 多播(组播): 一对多。例如:QQ聊天中群的一个人对群员发送信息
  • 广播:一对所有。应用场景例如:QQ上下线

UDP中使用广播,前提需要允许当前套接字发送广播。

# 发送广播数据的套接字进行修改设置,否则不能发送广播数据
udpSocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1) # 设置套接字配置

固定广播IP192.168.1.255或者是<broadcast>(尽量使用该方式)

#coding=utf-8


import socket, sys

dest = ('<broadcast>', 7788)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 对这个需要发送广播数据的套接字进行修改设置,否则不能发送广播数据
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)

# 以广播的形式发送数据到本网络的所有电脑中
s.sendto("Hi", dest)

print "等待对方回复(按ctrl+c退出)"

while True:
    (buf, address) = s.recvfrom(2048)
    print "Received from %s: %s" % (address, buf)

tftp文件下载器

wireshark流经电脑中的数据,都可以检测到。

cs架构client, server
bs架构browser, server

TFTP协议

TFTP(Trivial File Transfer Protocol,简单文件传输协议)

TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议

特点:

  • 简单
  • 占用资源小
  • 适合传递小文件
  • 适合在局域网进行传递
  • 端口号为69
  • 基于UDP实现

TFTP下载过程:

TFTP服务器默认监听69号端口
当客户端发送"下载"请求(即读请求)时,需要向服务器的69端口发送,服务器若批准此请求,则使用一个新的,临时的,端口进行数据传输。

clipboard.png

TFTP数据包的格式:

clipboard.png

确认包ACK都需要往随机端口发送数据.
上传和下载往69端口发送数据.

TFTP操作码:

操作码 功能
1 读请求,即下载
2 写请求,即上传
3 表示数据包,即DATA
4 确认码,即ACK
5 错误

packunpack的使用:

如何知道服务器发送完毕?
标记数据发送完毕:规定当客户端接收到到数据小于516字节(2字节操作码 + 2个字节到序号 + 512字节数据)时,意味着服务器发送完毕。

保证一个数字占用2个字节?
使用struct.pack()组包
使用struct.unpack()解包

组包:

sendData = struct.pack("!H8sb5sb",1,"test.jpg",0,"octet",0) # !表示网络数据, H占2个字节,8S占8个字节,b占一个字节。

解包:

udpSocket = socket(AF_INET, SOCK_DGRAM)
recvData = udpSocket.recvfrom(1024)
cmdTuple = struct.unpack('!HH', recvData[:4]) # 返回值 元组, 第一个元素操作码,第二个元素块编号

大端:在CPU中高位存储低位,CPU中低位存储高位
小端:在CPU中低位存储低位,CPU中高位存储高位
例如:ox11(高位) ox22(低位)
大端存储为:ox2211, 小端存储为: ox1122

from socket import *
import struct

udpSocket = socket(AF_INET, SOCKDGRAM)

# 发送请求数据
senData = struct.pack('!H8sb5sb', 1, 'test.jpg', 0, 'octet',0)
sendAddr = ('192.168.1.201', 8080)
socket.sendTo(senData, senAddr)

# 确认

# 接收数据
recvData = udpSocket.recvfrom(1024)
cmdTuple = struct.unpack('!HH', recvData[:4])
print(cmdTuple)

客户端:

#coding=utf-8
from socket import *

# 创建socket
tcpClientSocket = socket(AF_INET, SOCK_STREAM)

# 链接服务器
serAddr = ('192.168.1.102', 7788)
tcpClientSocket.connect(serAddr)

while True:
    # 提示用户输入数据
    sendData = input("send:")

    if len(sendData) > 0:
        tcpClientSocket.send(sendData)
    else:
        break

    # 接收对方发送过来的数据,最大接收1024个字节
    recvData = tcpClientSocket.recv(1024)
    print 'recv:',recvData

# 关闭套接字
tcpClientSocket.close()

服务端:

#coding=utf-8
from socket import *

# 创建socket
tcpSerSocket = socket(AF_INET, SOCK_STREAM)

# 绑定本地信息
address = ('', 7788)
tcpSerSocket.bind(address)

# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcpSerSocket.listen(5)

while True:
    # 如果有新的客户端来链接服务器,那么就产生一个信心的套接字专门为这个客户端服务器
    # newSocket用来为这个客户端服务
    # tcpSerSocket就可以省下来专门等待其他新客户端的链接
    newSocket, clientAddr = tcpSerSocket.accept()

    while True:
        # 接收对方发送过来的数据,最大接收1024个字节
        recvData = newSocket.recv(1024)

        # 如果接收的数据的长度为0,则意味着客户端关闭了链接
        if len(recvData) > 0:
            print('recv:', recvData)
        else:
            break

        # 发送一些数据到客户端
        sendData = input('send:')
        newSocket.send(sendData)

    # 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
    newSocket.close()

# 关闭监听套接字,只要这个套接字关闭了,就意味着整个程序不能再接收任何新的客户端的连接
tcpSerSocket.close()

TCP

UDP用户数据包协议模型中,在通信开始之前,不需要建立相关的链接,值需要发送数据即可,类似生活中的写信
TCP传输控制协议通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似生活中的打电话

TCP协议特点:

  • 稳定
  • 慢 (相对于udp而言,要慢一些,但是几乎提现微乎其微)
  • web服务器

UDP模型:

clipboard.png

TCP模型:

clipboard.png

socket创建出来的默认是主动套接字(套接字默认是给别人发送信息,而不是等待信息),listen()由主动变为被动。(转为被动套接字后,才可以收别人发送的数据)

TCP服务端步骤:

  1. 买手机: socket()
  2. 绑定手机卡: bind()
  3. 设置手机响铃模式: listen()
  4. 等待别人打电话接听: accept()
  5. 交流说话:recv()/send()接收发送数据

TCP客户端步骤:

  1. 买手机:socket()
  2. 拨打电话: connect()
  3. 交流说话:recv()/send()接收发送数据

newScoket, clientAddr = tcpSocket.accept(): 返回值是新的套接字(新的客户端)和新客户端的地址与ip

newScoket作用,去处理当前的请求业务,而主套接字tcpSocket,作为继续监听套接字。

TCP服务端:

#coding=utf-8
from socket import *


tcpSerSocket = socket(AF_INET, SOCK_STREAM)

# 绑定本地信息
address = ('', 7788)
tcpSerSocket.bind(address)

# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接
tcpSerSocket.listen(5)

# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务器,newSocket用来为这个客户端服务,tcpSerSocket就可以省下来专门等待其他新客户端的链接
newSocket, clientAddr = tcpSerSocket.accept()

# 接收对方发送过来的数据,最大接收1024个字节
recvData = newSocket.recv(1024)
print '接收到的数据为:',recvData

# 发送一些数据到客户端
newSocket.send("thank you !")

# 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
newSocket.close()

# 关闭监听套接字,只要这个套接字关闭了,就意味着整个程序不能再接收任何新的客户端的连接
tcpSerSocket.close()

TCP客户端:

#coding=utf-8
from socket import *

tcpClientSocket = socket(AF_INET, SOCK_STREAM)

# 链接服务器
serAddr = ('192.168.1.102', 7788)
tcpClientSocket.connect(serAddr) # 链接服务器需要耗费时间

sendData = input("请输入要发送的数据:")

tcpClientSocket.send(sendData)

# 接收对方发送过来的数据,最大接收1024个字节
recvData = tcpClientSocket.recv(1024)
print('接收到的数据为: ', recvData)

# 关闭套接字
tcpClientSocket.close()

Note:
TCP客户端已经链接好了服务器,在以后的数据发送中,不需要填写对方的ipport
UDP在发送数据的时候,因为没有之前的链接,在每次发送的数据的时候,需要每次填写接收方的ipport

模拟QQ聊天:

客户端:

from scoket import *


cSocket = socket(AF_INET, SOCK_STREM)

addr = ('192.168.1.201', 8180)
cSocket.connect(addr)

while True:
        sendData = input('data: ')

        if len(sendData) > 0:
                cSocket.send(sendData)
        else:
                break

        # 接收对方发送的数据,最大值1024个字节
        recvData = cSocket.recv(1024)

        print('return data: %s'%recvData)

cSocket.close()

服务端:

#coding=utf-8
from socket import *

sSocket = socket(AF_INET, SOCK_STREM)

adder = ('', 8180)
sSocket.bind(adder)
sSocket.listen(5)

while True:
        newSocket, clientAddr = sSocket.accept()

        while True:
                recvData = newSocket.recv(1024)
                    
                # 如果接收到客户端发送的数据为0,表示客户端已经下线
                if len(recvData) > 0: 
                        print('recv: ', recvData)
                else:
                        break # 退出
        newSocket.close() # 关闭新的Socket


sSocket.close()

客户端一般绑定IP, 服务器一般不写IP

网络通信过程

名词:

Routers: 路由器
Switches:交换机
Hubs:集线器
Wireless Devices: 无线设备
Connections:连接器
End Devices:终端设备

辅助软件: Cisco Packet Tracer

通过集线器组网
  • hub(集线器)作用: 能够完成多个电脑的链接
  • 每个数据包的发送都是以广播的形式进行的,容易堵塞网络(任何数据,每次都是以广播形式发送)

不允许一条网线之间有三台或以上的电脑一起链接,会导致数据混乱。需要使用hub(集线器),交换机等设备解决。

网络掩码:
C类默认掩码:255.255.255.0
B类默认掩码:255.255.0.0
A类默认掩码:255.0.0.0
网络掩码必须和IP一齐出现,才有作用。

网络掩码作用:网络掩码按位与IP地址 => 网络号

网络号相同处于同一个网段,才可以通信。

通过交换机组网

网络交换机介绍:
网络交换机(又称“网络交换器”),是一个扩大网络的器材,能为子网络提供更多的连接端口,以便连接更多的计算机,具有性能比高,高度灵活,相对简单,易于实现等特点。
以太网技术已

交换机的作用:

  • 转发过滤:当一个数据帧的目的地址在MAC地址表中有映射时,它被转发到连接目的节点的端口而不是所有端口(如该数据帧为广播帧则转发至所有端口)
  • 学习功能:以太网交换机了解每一端口相连设备的MAC地址,并将地址同相应的端口映射起来存放在交换机缓存中的MAC地址表中成为当今最重要的一种局域网组网技术,网络交换机也就成为了最普及的交换机。

Note:

  • 如果PC不知目标IP所对应的的MAC,那么可以看出,pc会先发送arp广播,得到对方的MAC然后,在进行数据的传送
  • 当switch第一次收到arp广播数据,会把arp广播数据包转发给所有端口(除来源端口);如果以后还有pc询问此IP的MAC,那么只是向目标的端口进行转发数据
交换机和集线器区别

交换机和集线器相同点:

  • 完成多台电脑的链接

交换机和集线器不同点:

  • 交换机第一次以广播形式确认具体当前是哪台电脑(学习功能),后面都是点对点的发送数据包(转发过滤功能)。(学习之后都是以单播形式传输数据包)
  • 集线器不管是第一次还是以后多次,还是对方电脑返回数据包,都是以广播形式发送。(容易造成网络拥堵)
arp和icmp

网卡有一组序列号:
实际地址/硬件地址/MAC地址: 6组数据,每一组数据都是十六进制表示。一共有六个字节。
六个字节分为,前三组(厂商地址),后三组(该厂商生产的网卡序列号)。

clipboard.png

ping 192.168.1.1使用的是ICMP协议
获取MAC地址号使用的是ARP协议

ping之前并不知道对方的MAC地址,需要先使用ARP协议,然后使用ICMP协议。

clipboard.png

OSI Model 对应七层分类。

七层分类:物理层 -> 数据链路层 -> 网络层 -> 传输层 -> 会话层 -> 表示层 -> 应用层

ICMP协议作用:ping 命令使用
ARP协议作用:获取MAC地址(广播),根据IP寻找MAC地址
RARP协议作用: 根据MAC地址寻找IP

广播的MAC地址: FFFF.FFFF.FFFF, TYPE0x806

clipboard.png

arp -a命令:每台pc都会有一个arp缓存表,用来记录IP所对应的的MAC。`
arp -d命令:删除arp缓存表

路由器的作用以及组网

路由器,确定一条路径的设备。(假想成十字路口的路标)

功能:

  • 链接不同的网络,不同网段之间通信。
  • 判断网络地址
  • 选择IP路径

路由器特点:

  • 至少有两个网卡(一个网卡具有一个IP地址)
  • 两个网卡都是在一个设备上

Note:

  • 不在同一网段的pc,需要设置默认网关才能把数据传送过去 通常情况下,都会把路由器默认网关
  • 当路由器收到一个其它网段的数据包时,会根据“路由表”来决定,把此数据包发送到哪个端口;路由表的设定有静态和动态方法
  • 每经过一次路由器,那么TTL值就会减一

路由解析协议:RIP

网络通信过程的mac地址以及ip的不同

有了IP为何需要MAC地址:
获取默认网关的MAC地址(rap协议

MAC地址以及IP的不同:

  • MAC地址,在两个设备之间通信时,在实时变化。
  • IP地址,在整个通讯过程中,都不变化。

IP:标记逻辑上的地址
MAC:标记实际转发设备的地址
netmask网络掩码: 和IP地址一起确定网络号
默认网关:发送的IP不在同一个网段内,那么会把这个数据转发给,默认网关。(每台电脑,服务器都会配置默认网关)

route print: 查看路由表

clipboard.png

pc + switch + router + server
  • DNS服务器用来解析出IP(类似电话簿)
  • DFGATEWAY(默认网关)用来对顶,当发送的数据包的目的ip不是当前网络时,此数据包包转发的目的ip
  • 在路由器中路由表指定数据包的”下一跳”的地址

server(服务器): 主机

访问baidu的过程:
所有访问都是第一次:

  • 先知道默认网关的MAC地址:

    1. 使用ARP协议获取默认网关MAC地址
    2. 组织数据 发送给默认网关(IP还是DNS服务器的IP,但是MAC地址是默认网关的MAC地址)
    3. 默认网关拥有把转发数据的能力,把数据转发给路由器
    4. 路由器根据自己的路由协议,选择一个合适的较快的路径,转发给目的网关(DNS所在的网关)
    5. 目的网关把数据转发给DNS服务器
    6. DNS服务器查询解析出www.baidu.com对应IP的地址,并原路返回给请求这个域名的client
  • 得到www.baidu.com对应的IP地址之后,会发送TCP3次握手,并进行连接。
  • 使用HTTP发送请求数据给Web服务器
  • Web服务器收到请求数据之后,通过查询自己的服务器得到相应的结果,原路返回给浏览器。
  • 浏览器接收到数据之后,通过浏览器自己的渲染功能显示
  • 浏览器关闭TCP链接,即4次挥手

域名还是IP访问: IP访问三次握手,然后发送真正请求数据;域名访问DNS服务器,再然后三次握手,最后发送真正请求数据。

DHCP协议:自动分配地址,当前网络的电脑中没有ip地址,自动分配。

DNS服务器

作用:解析域名
使用协议:UDP

pc配置:
默认网关,DNS服务器,IP地址,网络掩码

clipboard.png

clipboard.png

router配置:
不同网段IP地址(充当默认网关),多台路由器IP

clipboard.png

clipboard.png

server配置:
IP地址,网络掩码,各种协议配置

clipboard.png

clipboard.png

tcp三次握手,四次挥手

clipboard.png

sequeue num序列号, ack num确认包 等值的变化:

SYN: 标记的TCP整个包的作用,也就是请求。
SYN + ACK: 返回包的的格式
ACK: 第二次请求格式

clipboard.png

clipboard.png

clipboard.png

第二次请求格式,还一并携带的数据包,而后client会发送确认数据包,并且client会自动关闭套接字,告知服务器。

PSH + ACK: 第二次携带数据包格式
FIN + ACK: 浏览器套接字关闭发送的包(服务端和客户端各自调用套接字的close都会发送一个包告知对方)

clipboard.png

clipboard.png

三次握手作用:建立链接,保存信息。

TCP十种状态

netstat -n: 显示协议统计信息和当前 TCP/IP 网络连接,。
-n参数: 以数字形式显示地址和端口号

只要客户端调用close,服务器的recv的数据长度为0,
过一段时间客户端才会close,而服务器收到TIME_WAIT的包之后,就关闭socket

clipboard.png

Note:

  • 当一端收到一个FIN,内核让read返回0来通知应用层另一端已经终止了向本端的数据传送
  • 发送FIN通常是应用层对socket进行关闭的结果
tcp的2MSL问题

clipboard.png

TTL: 一个数据包在网络上经过的路由器的最大值,经过路由器的个数。

  • 如果路由接收到的TTL值是0的话,不会转发当前数据包,会直接扔掉。
  • 每经过一个路由减1(从此路过,留下1)
UNIX 及类 UNIX 操作系统 ICMP 回显应答的 TTL 字段值为 255
Compaq Tru64 5.0 ICMP 回显应答的 TTL 字段值为 64
微软 Windows NT/2K操作系统 ICMP 回显应答的 TTL 字段值为 128 
微软 Windows 95 操作系统 ICMP 回显应答的 TTL 字段值为 32

MSL: 一个数据包在网络上存储的最长时间(1min-2min)。
2MSL:2倍的存活最长时间(2min-4min)。

当第一次关闭,客户端没有ACK的时候,过一段时间服务器会再次发送FIN,最终收到这个数据包之后,就关闭掉通信。
那边先close(不管是client还是server)就会等待2MSL时间。在这个时间中,这个套接字不会被释放,然后重启服务器的时候,导致绑定失败

2MSL问题原因:当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次握手完成后发送了第四次握手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。TIME_WAIT状态 时两端的端口不能使用,要等到2MSL时间结束才可继续使用。

导致结果:当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃

解决方法:可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

长连接、短连接

TCP在真正的读写操作之前,serverclient之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,连接的建立通过三次握手,释放则需要四次握手,所以说每个连接的建立都是需要资源消耗和时间消耗的。

长连接:
一次TCP三次握手,发送数据,一直发送数据,最后四次握手关闭。 例如:观看视频
短链接:
TCP三次握手,发送数据,四次握手关闭。重新TCP三次握手,发送数据,四次握手关闭。如此反复。例如:访问网页

TCP长/短连接的优点和缺点:

  • 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用于长连接。
  • 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。
  • clientserver之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个的客户端连累后端服务。

TCP长/短连接的应用场景:

  • 长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三次握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,再次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
  • 而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。
listen的队列长度

listen参数问题: 首次一次达到该设置参数数值,后面关闭之后,已连接队列扔出一个,才能继续进行,再从半连接队列到已连接队列中。

tcpSerSocket.listen(connNum) 
# connNum表示,半链接和已链接次数的总长度
# 在Linux中不管写多少,都是系统会自己计算该值
# Mac电脑系统上,用户写多少就是多少。

clipboard.png

服务端:


#coding=utf-8
from socket import *
from time import sleep

# 创建socket
tcpSerSocket = socket(AF_INET, SOCK_STREAM)

# 绑定本地信息
address = ('', 7788)
tcpSerSocket.bind(address)

connNum = int(raw_input("请输入要最大的链接数:"))

# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
tcpSerSocket.listen(connNum)

# 在这个期间,如果有20个客户端调用了connect链接服务器,那么这个服务器的Linux底层,会自动维护2个队列(半链接和已链接)
# 其中,半链接和已链接的总数为linsten中的值,如果这个值是5,那么,意味着此时最多只有5个客户端能够链接成功,而剩下15则会堵塞在connect函数
for i in range(10):
    print(i)
    sleep(1)

while True:

    # 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务器
    newSocket, clientAddr = tcpSerSocket.accept() # 如果服务器调用了accept,那么Linux底层中的那个半链接和已链接中的总数就减少了一个,因此,此时的15个因为connect堵塞的客户端又会在进行连接 来争抢1个刚空出来的空位。
    print clientAddr
    sleep(1)

客户端:

#coding=utf-8
from socket import *

connNum = raw_input("请输入要链接服务器的次数:")
for i in range(int(connNum)):
    s = socket(AF_INET, SOCK_STREAM)
    s.connect(("192.168.1.102", 7788))
    print(i)

Note:

  • listen中的black表示已经建立链接和半链接的总数
  • 如果当前已建立链接数和半链接数以达到设定值,那么新客户端就不会connect成功,而是等待服务器
手动配置ip

设置IP和掩码:

ifconfig eth0 192.168.5.40 netmask 255.255.255.0

设置网关:

route add default gw 192.168.5.1
常见网络攻击

tcp半链接攻击

tcp半链接攻击(SYN Flood (SYN洪水)):是种典型的DoS (Denial of Service,拒绝服务) 攻击

导致结果:服务器TCP连接资源耗尽,停止响应正常的TCP连接请求。

三次链接的第一次就是半链接

dns攻击

dns使用的是udp协议,不稳定.

dns服务器被劫持:
需要攻击dns服务器,或者和dns服务器合作。
一个域名服务器对其区域内的用户解析请求负责,但是并没有一个机制去监督它有没有认真地负责。 有些被攻击dns服务器故意更改一些域名的解析结果,将用户引导向一个错误的目标地址。
用来阻止用户反问某些特定的网站,后者是将用户引导到广告页面,或者构造钓鱼网站,获取用户信息。

dns欺骗:
主动用一个假的dns应答来欺骗用户计算机,让其相信这个假的地址,并且抛弃真正的dns应答。

导致用户访问假的目的地址。

查看域名解析的ip地址方法

nslookup 域名

# 例如
nslookup baidu.com

clipboard.png

arp攻击

修改电脑的mac地址,使用中间人攻击。

clipboard.png

NAT

NAT: 网络地址转换器

家庭上网:
电话线 -> 调制解调器(猫) -> 路由器 -> 电脑,手机

调制解调器中出来的IP才可以访问到外网。

LAN口: 局域网
WAN口: 万维网

clipboard.png

路由器存在一张表,一个本地电脑和路由器对应到标识。

路由器功能:代理
本地电脑不能访问,路由器把不能访问的地址扔掉。

并发服务器

单进程服务器

简单单进程TCP服务器:

from socket import *

serSocket = socket(AF_INET, SOCK_STREAM)

# 重复使用绑定的信息
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 作用:服务器先四次挥手到第一次,最终也等待2MSL时间。 服务器先结束,而且立即运行服务器,就不会出现

localAddr = ('', 7788)

serSocket.bind(localAddr)

serSocket.listen(5)

while True:
    print('主进程,等待新客户端的到来')
    newSocket, destAddr = serSocket.accept()
    print('主进程,接下来负责数据处理[%s]'%str(destAddr))

    try:
        while True:
            recvData = newSocket.recv(1024)
            if len(recvData) > 0:
                print('recv[%s]:%s'%(str(destAddr), recvData))
            else:
                print('[%s]客户端已经关闭'%str(destAddr))
                break
    finally:
        newSocket.close() # 服务器主动关闭

serSocket.close()
关闭监听套接字、已连接套接字的不同

关闭监听套接字,意味着:不能再连接新的客户端连接。
已连接套接字关闭,意味着:当前套接字不能再使用sendrecv来收发数据。

COW写时拷贝:到该需要时才去拷贝,不然都是引用,能共有才去共用。

多进程服务器:

from socket import *
from multiprocessing import *
from time import sleep

# 处理客户端的请求并为其服务
def dealWithClient(newSocket,destAddr):
    while True:
        recvData = newSocket.recv(1024)
        if len(recvData)>0:
            print('recv[%s]:%s'%(str(destAddr), recvData))
        else:
            print('[%s]客户端已经关闭'%str(destAddr))
            break

    newSocket.close()


def main():

    serSocket = socket(AF_INET, SOCK_STREAM)
    serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
    localAddr = ('', 7788)
    serSocket.bind(localAddr)
    serSocket.listen(5)

    try:
        while True:
            print('主进程,等待新客户端的到来')
            newSocket,destAddr = serSocket.accept()

            print('主进程,接下来创建一个新的进程负责数据处理[%s]'%str(destAddr))
            client = Process(target=dealWithClient, args=(newSocket,destAddr))
            client.start()

            # 因为已经向子进程中copy了一份(引用),并且父进程中这个套接字也没有用处了
            # 所以关闭
            newSocket.close()
    finally:
        # 当为所有的客户端服务完之后再进行关闭,表示不再接收新的客户端的链接
        serSocket.close()

if __name__ == '__main__':
    main()
  • 通过为每个客户端创建一个进程的方式,能够同时为多个客户端进行服务
  • 当客户端不是特别多的时候,这种创建进程方式还可以,如果有几百上千,就不可取了,因为每次创建进程等过程需要较大的资源。

多线程服务器:

#encode=utf-8
from socket import *
from multiprocessing import *
from time import sleep


# 客户端的请求并为其服务

def dealWithClient ():
        while True:
                recv_data = new_socket.recv(1024)
                if len(recv_data) > 0:
                        print('recv[%s]:%s'%(str(destAddr), recvData))
                else:
                        print('[%s]客户端已经关闭'%str(destAddr))
                        break
                new_socket.close()


def main ():
        ser_socket = socket(AF_INET, SOCK_STREAM)
        ser_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        local_addr = ('', 7788)
        ser_socket.bind(local_addr)
        ser_socket.listen(5)

        try:
                while True:
                        print('等待客户端')
                        new_socket, dest_addr = ser_socket.accpet()

                        print('父进程[%s]'%str(dest_addr))
                        client = Thread(target=deal_width_client, args=(new_socket, dest_addr))
                        client.start()
        finally:
                ser_socket.close()

if __name__ == '__main__':
        main()
单进程服务器-非堵塞模式
#coding=utf-8
from socket import *
import time

# 用来存储所有的新链接的socket
g_socketList = []

def main():
    serSocket = socket(AF_INET, SOCK_STREAM)
    serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
    localAddr = ('', 7788)
    serSocket.bind(localAddr)
    # 可以适当修改listen中的值来看看不同的现象
    serSocket.listen(1000)
    # 将套接字设置为非堵塞
    # 设置为非堵塞后,如果accept时,恰巧没有客户端connect,那么accept会
    # 产生一个异常,所以需要try来进行处理
    serSocket.setblocking(False)

    while True:

        try:
            newClientInfo = serSocket.accept()
        except Exception as result:
            pass
        else:
            print("一个新的客户端到来:%s"%str(newClientInfo))
            newClientInfo[0].setblocking(False)
            g_socketList.append(newClientInfo)

        # 用来存储需要删除的客户端信息
        needDelClientInfoList = []

        for clientSocket,clientAddr in g_socketList:
            try:
                recvData = clientSocket.recv(1024)
                if len(recvData)>0:
                    print('recv[%s]:%s'%(str(clientAddr), recvData))
                else:
                    print('[%s]客户端已经关闭'%str(clientAddr))
                    clientSocket.close()
                    g_needDelClientInfoList.append((clientSocket,clientAddr))
            except Exception as result:
                pass

        for needDelClientInfo in needDelClientInfoList:
            g_socketList.remove(needDelClientInfo)

if __name__ == '__main__':
    main()
select版服务器

select作用: 完成IO的多路复用。能够完成对一些套接字的检测(所有的套接字 )。

多路复用:在没有开辟多进程,多线程的情况下, 能够完成并发服务器的开发。

readable, writeable, exceptionsal = select.select(inputs, [], [])

select参数:
第一个列表:检测当前列表是否可以收数据。
第二个列表:检测当前列表是否可以发数据。
第三个列表:检测当前列表是否产生了异常。

#eoding=utf-8
import select
import sys
import socket


running = True

def main ():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('', 7788))
    server.listen(5)

    inputs = [server]

    while True:
      readable, writeable, exceptionsal = select.select(inputs, [], [])

      for sock in readable:
        if sock == server:
          client, addr = server.accept()
          inputs.append(client)
        
        # 监听用户输入的键盘
        elif sock == sys.stdin:
          cmd = sys.stdin.readline()
          running = False
          break

        else:
          data = sock.recv(1024)
          if data:
            sock.send(data)
          else:
            inputs.remove(data)
            sock.close()

      if not running:      
        break

    server.close()    

if __name__ == '__main__':
    main()

优点:
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点:

  • select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
  • 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max查看。32位机默认是1024个。64位机默认是2048.
  • socket进行扫描时是依次扫描的,即采用轮询的方法,效率较低。
  • 当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZESocket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。
epoll版服务器

select: 轮询,大小限制
poll: 轮询
epoll: 使用事件通知机制

clipboard.png

文件描述符:fileno(), 就是数字,文件创建的时候对应的生成唯一数字。

sys.stdout # 标准输出(屏幕)
sys.stdin # 标准输入(键盘)
sys.stderr # 标准错误(屏幕)

epoll的优点:

  • 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024
  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于selectpoll
#coding=utf-8
import socket
import select

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('', 1005))
    s.listen()

    epoll = select.epoll()

    # 注册事件到epoll中
    # epoll.register(fd[, eventmask])
    # 如果fd已经注册过,则会发生异常
    # 将创建的套接字添加到epoll的事件监听中
    epoll.register(s.fileno(), select.EPOLLIN|select.EPOLLET)

    connections = {}
    addresses = {}

    while True:
        # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
        epoll_list = epoll.poll()

        # 对事件进行判断
        for fd, events in epoll_list:
            if fd === s.lineno(): # 确定当前那个套接字被激活
                client, addr = s.accept()
                print('new client %s'%str(addr))

                # 将addr和套接字添加到字典中, fd作为当前字典的key
                connections[client.fileno()] = client
                addresses[client.fileno()] = addr

                # 向 epoll中注册 连接的socket的可读事件
                epoll.register(client.fileno(), select.EPOLLIN|select.EPOLLET)
            elif events == select.EPOLLIN: # 判断事件是否是可接收数据的事件
                recvdata = connections[fd].recv(1024)

                if len(recvdata) > 0:
                    print('recv: %s'%recvdata)
                else:
                    # 移除注册
                    epoll.unregister()   
                    # 关闭套接字
                    connections[fd].close()

                    print('%s---offline---'%str(addresses[fd]))

if __name__ == '__main__':
    main()

参数说明(特殊标识的数字):

  • EPOLLIN (可读)
  • EPOLLOUT (可写)
  • EPOLLET (ET模式)

ET模式区别:

  • LT(level trigger)ET(edge trigger)LT模式是默认模式。
  • LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知并通知此事件。(边缘触发, 多次通知)
  • ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。(平行触发,只通知一次)

协程

协程(微线程):在一个线程中把任务分成N份,底层是通过生成器实现。

计算密集型: 需要占用大量CPU (使用多进程)
IO密集型: 时间花在等待上(使用多线程和协程)

协程其实可以认为是比线程更小的执行单元。
为什么是执行单元:自带CPU上下文,这样只要在何时的时机,可以把一个协程切换到另一个协程。 只要这个过程中保存或恢复CPU上下文,那么程序还是可以运行到。

协程的问题:
系统并不感知,所以操作系统不会帮你做切换。 那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间。

协程的简单实现:

import time

def A():
    while True:
        print("----A---")
        yield
        time.sleep(0.5)

def B(c):
    while True:
        print("----B---")
        next(c)
        time.sleep(0.5)

if __name__=='__main__':
    a = A()
    B(a)
greenlet实现多任务

greenlet模块作用: 使用协程来完成多任务的切换。

#coding=utf-8

from greenlet import greenlet
import time

def test1():
    while True:
        print('---A---')
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print('---B---')
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

# 切换到gr1中运行
gr1.switch()
gevent版服务器

greenlet缺点:需要开发者自己切换。

gevent: 能够自动切换任务模块

原理:当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其它到greenlet,等到IO操作完成,再在适当到时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为其自动切换协程,就保证总有greenlet在运行,而不是等待IO

import gevent

def f(n):
    for i in range(n):
        print gevent.getcurrent(), i
        # 用来模拟一个耗时操作,注意不是time模块中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

HTTP协议

HTTP是无状态

浏览器和服务器之间的传输协议是HTTP

浏览器请求:

GET / HTTP/1.1
Host: www.sina.com

GET表示一个读取请求,将从服务器获得网页数据,/表示URL的路径,URL总是以/开头,/就表示首页,最后的HTTP/1.1指示采用的HTTP协议版本是1.1。
目前HTTP协议的版本就是1.1,但是大部分服务器也支持1.0版本,主要区别在于1.1版本允许多个HTTP请求复用一个TCP连接,以加快传输速度。

服务器响应:

HTTP响应分为HeaderBody两部分(Body是可选项)

HTTP/1.1 200 OK
Content-Type: text/html

200表示一个成功的响应,后面的OK是说明。
Content-Type指示响应的内容,这里是text/html表示HTML网页。

HTTP格式

每个HTTP请求和响应都遵循相同的格式,一个HTTP包含HeaderBody两部分,其中Body是可选的。

HTTP协议是一种文本协议

HTTP GET请求的格式:

GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

每个Header一行一个,换行符是\r\n

HTTP POST请求的格式:

POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...

当遇到连续\r\n\r\n时,Header部分结束,后面的数据全部是Body

HTTP响应的格式:

HTTP1.1 200 OK
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...

HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。

显示静态文件:

#coding=utf-8
import socket
import re

from multiprocessing import Process


# 设置静态文件根目录
HTML_ROOT_DIR = './html'

def hand_client(cli_sock):
    request_data = cli_sock.recv(1024)
    print('--- requset_data %s ---'%request_data)
    request_lines = request_data.splitlines()

    for line in request_lines:
        print(line)
    
    # 解析请求报文
    # GET / HTTP/1.1
    request_start_line = request_lines[0]
    file_name = re.match(r'\w+\s+(/[^ ]*)', request_start_line.decode('utf-8')).group(1)
    if '/' == file_name: # 常量写在右边,变量写在左边
        file_name = '/index.html'

    # 打开文件,读取内容
    try:
        file = open(HTML_ROOT_DIR + file_name, 'rb')
    except IOError:
        response_data_line = 'HTTP1.1 404 not found\r\n'
        response_data_head = 'Server: alogy server\r\n'
        response_data_body = 'the file is not found!'
    else:            
        file_data = file.read()
        file.close()    
        # 构造客户端返回数据
        response_data_line = 'HTTP1.1 200 OK\r\n'
        response_data_head = 'Server: alogy server\r\n'
        response_data_body = file_data.decode('utf-8')

    response = response_data_line + response_data_head + '\r\n' + response_data_body
    print('response data:', response)
    cli_sock.send(bytes(response, 'utf-8'))
    cli_sock.close()

def main():
    ser_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ser_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    ser_sock.bind(('', 7329))
    ser_sock.listen(127)

    while True:
        cli_sock, cli_address = ser_sock.accept()
        print('[%s, %s]用户链接了'%cli_address)
        hand_process = Process(target=hand_client, args=(cli_sock, ))
        hand_process.start()
        cli_sock.close()

if __name__ == '__main__':
    main()
 

文件打开方式_文本与二进制的区别:

\n -> linux
\r\n -> windows(文本识别\r\n, 二进制不识别\n)

不需要从正向思维考虑,定义类的时候,从怎么调用开始。

使用类的方式显示静态文件

#coding=utf-8
import socket
import re

from multiprocessing import Process


# 设置静态文件根目录
HTML_ROOT_DIR = './html'

class HttpServer(object):
    def __init__(self):
        self.ser_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ser_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    def bind(self, port):
        self.ser_sock.bind(('', port))

    def start(self):
        self.ser_sock.listen(128)

        while True:
            cli_sock, cli_address = self.ser_sock.accept()
            print('[%s, %s]用户链接了'%cli_address)
            hand_process = Process(target=self.hand_client, args=(cli_sock, ))
            hand_process.start()
            cli_sock.close()

    def hand_client(self, cli_sock):
        request_data = cli_sock.recv(1024)
        print('--- requset_data %s ---'%request_data)
        request_lines = request_data.splitlines()

        for line in request_lines:
            print(line)
        
        # 解析请求报文
        # GET / HTTP/1.1
        request_start_line = request_lines[0]
        file_name = re.match(r'\w+\s+(/[^ ]*)', request_start_line.decode('utf-8')).group(1)
        if '/' == file_name: # 常量写在右边,变量写在左边
            file_name = '/index.html'

        # 打开文件,读取内容
        try:
            file = open(HTML_ROOT_DIR + file_name, 'rb')
        except IOError:
            response_data_line = 'HTTP1.1 404 not found\r\n'
            response_data_head = 'Server: alogy server\r\n'
            response_data_body = 'the file is not found!'
        else:            
            file_data = file.read()
            file.close()    
            # 构造客户端返回数据
            response_data_line = 'HTTP1.1 200 OK\r\n'
            response_data_head = 'Server: alogy server\r\n'
            response_data_body = file_data.decode('utf-8')

        response = response_data_line + response_data_head + '\r\n' + response_data_body
        print('response data:', response)
        cli_sock.send(bytes(response, 'utf-8'))
        cli_sock.close()        

def main():
    http_server = HttpServer()
    http_server.bind(7329)
    http_server.start()

if __name__ == '__main__':
    main()
 
WEGI协议

作用:不修改服务器和架构代码而确保可以在多个架构下运行web服务器

定义WSGI接口:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return 'Hello World!'

environ:一个包含所有HTTP请求信息的dict对象;
start_response:一个发送HTTP响应的函数。

调用WSGI服务器:
application函数

WSGI服务器:

#coding=utf-8
import socket
import re
import sys

from multiprocessing import Process


# 设置静态文件根目录
HTML_ROOT_DIR = './html'

WSGI_PYTHON_DIR = './wsg_python'

class HttpServer(object):
    def __init__(self):
        self.ser_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.ser_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    def bind(self, port):
        self.ser_sock.bind(('', port))

    def start(self):
        self.ser_sock.listen(128)

        while True:
            cli_sock, cli_address = self.ser_sock.accept()
            print('[%s, %s]用户链接了'%cli_address)
            hand_process = Process(target=self.hand_client, args=(cli_sock, ))
            hand_process.start()
            cli_sock.close()

    def hand_client(self, cli_sock):
        request_data = cli_sock.recv(1024)
        print('--- requset_data %s ---'%request_data)
        request_lines = request_data.splitlines()

        for line in request_lines:
            print(line)
        
        # 解析请求报文
        # GET / HTTP/1.1
        request_start_line = request_lines[0]
        file_name = re.match(r'\w+\s+(/[^ ]*)', request_start_line.decode('utf-8')).group(1)
        method = re.match(r'(\w+)\s+/[^ ]*', request_start_line.decode('utf-8')).group(1)

        if file_name.endswith('.py'):
            try:
                module = __import__(file_name[1:-3])
            except Exception:
                self.response_headers = 'HTTP/1.1 404 Not Found\r\n'
                response_body = 'not found'
            else:
                env = {
                    "PATH_INFO": file_name,
                    "METHOD": method
                }
                # 容错处理
                response_body = module.application(env, self.start_response)
            response = self.response_headers + '\r\n' + response_body
        else:    
            if '/' == file_name: # 常量写在右边,变量写在左边
                file_name = '/index.html'
            # 打开文件,读取内容
            try:
                file = open(HTML_ROOT_DIR + file_name, 'rb')
            except IOError:
                response_data_line = 'HTTP1.1 404 not found\r\n'
                response_data_head = 'Server: alogy server\r\n'
                response_data_body = 'the file is not found!'
            else:            
                file_data = file.read()
                file.close()    
                # 构造客户端返回数据
                response_data_line = 'HTTP1.1 200 OK\r\n'
                response_data_head = 'Server: alogy server\r\n'
                response_data_body = file_data.decode('utf-8')

            response = response_data_line + response_data_head + '\r\n' + response_data_body
            print('response data:', response)
        
        cli_sock.send(bytes(response, 'utf-8'))
        cli_sock.close()   
    def start_response(self, status, headers):
        server_headers = [
            ('Server', 'My server')
        ]
        response_headers = 'HTTP/1.1 ' + status + '\r\n'
        for header in headers:
            response_headers += '%s: %s\r\n'%header
        self.response_headers = response_headers

def main():
    sys.path.insert(1, WSGI_PYTHON_DIR)
    http_server = HttpServer()
    http_server.bind(7329)
    http_server.start()

if __name__ == '__main__':
    main()

WSGI接口:


#coding=utf-8
import time


def application(env, start_response):
    status = '200 OK' # 当前状态
    headers = [ # 响应头
        ('Conent-type', 'text/plain')
    ]
    env.get('PATH_INFO')
    env.get('METHOD')
    start_response(status, headers) # 状态和响应头需要通过函数调用参数传递
    return time.ctime() # 响应体 # 只能返回响应体
Web框架

框架的骨架:

固定细节已经实现好,使用者补充需要的部分。

#coding=utf-8


class Application(object):
    '''框架主要部分'''
    def __init__(self, urls):
        self.urls = urls # 路由表
    def __call__(self, env, start_response):
        path = env.get('PATH_INFO', '/')
        for url, handler in self.urls:
            if path == url:
                return handler(env, start_response)
            # response_body = handler(env, start_response)
        # 404 路由信息
        status = '404 not found'
        headers = []
        start_response(status, headers)    
        return 'not found'    

alogy
1.3k 声望121 粉丝

// Designer and Developer


引用和评论

0 条评论