Python网络编程

TCP/IP协议族
这里写图片描述

用端口在网络上区分进程。大于1024的端口是动态端口,小于的是操作系统预留端口。
私有IP:
这里写图片描述

Socket

import socket
#TCP
tcpSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#UDP
udpSocket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udpScoket.sendto("HelloWorld",("192.168.0.2",8080))  #发送的端口随机,可以使用udpSocket.bind来绑定端口
#一般发送方不需要绑定,接收方需要绑定

端口绑定

#绑定本地的相关信息,如果一个网络程序不绑定,则系统会随机分配
bindAddr = ('',5566)   #IP地址和端口号
udpSocket.bind(bindAddr)
revData = udpSocket.recvfrom(1024) #表示本地接受的最大字节数
print(revData)
  • 单工。收音机
  • 半双工。对讲机
  • 全双工。电话,Socket

python3下的文本编码问题

str.decode('utf-8')
str.decode('gb2312')

TFTP协议

这里写图片描述

from socket import *
import struct
sendData = struct.pack("!H8sb5sb",1,"test.jpg",0,"octet",0)

大端小端
- 大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;一般PC机使用
- 小端模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。一些服务器使用

!H8sb5sb:!表示使用网络字节序,即大端。H表示两个字节,类似%d,s表示一个字节,8s表示8个字节,也可写成ssssssss;0替换b占用的一个字节。

UDP广播

单播(点对点)、多播(一对多)、广播(对所有)。

import socket
#192.168.1.0网络号  则192.168.1.255广播地址
dest = ('<broadcast>','7788')#<broadcast>自动识别网络号
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
#发送广播需要这句话
s.setsockopt(socket.SOL_SOCKET,socket.S5_BROADCAST,1)
s.sendto('hello im braodcast',dest)

广播只能在UDP中使用。
UDP类似于写信,TCP类似打电话。

TCP

相对UDP
1、稳定
2、慢一些
3、Web服务器都是使用TCP。

UDP通信模型

这里写图片描述

TCP通信模型

这里写图片描述

TCP服务器

from socket import *

serverSocket = socket(AF_INET, SOCK_STREAM)

serverSocket.bind(("", 8899))

serverSocket.listen(5)

print("-----1-----")
clientSocket,clientInfo = serverSocket.accept()

print("-----2-----")
#clientSocket 表示这个新的客户端
#clientInfo 表示这个新的客户端的ip以及port

recvData = clientSocket.recv(1024)

print("-----3-----")
print("%s:%s"%(str(clientInfo), recvData))

clientSocket.close()
serverSocket.close()

TCP客户端

from socket import *

clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect(("192.168.1.108", 8989))

#注意:
# 1. tcp客户端已经链接好了服务器,所以在以后的数据发送中,不需要填写对方的iph和port----->打电话
# 2. udp在发送数据的时候,因为没有之前的链接,所依需要 在每次的发送中 都要填写接收方的ip和port----->写信 
# 或者写成 b"haha"
clientSocket.send("haha".encode("gb2312"))

recvData = clientSocket.recv(1024)

print("recvData:%s"%recvData)

clientSocket.close()

网络掩码

这里写图片描述
网络掩码与IP地址用二进制按位与得到网络号。
如192.168.10.102 & 255.255.255.0 = 192.168.10.0 。
网络号相同的多台终端可以相互通信。

连接多台电脑的方法:
1、集线器:现在已经被淘汰
2、交换机
两者区别:
集线器收到的所有数据包都会以广播形式发送;交换机不是。

ping命令使用的是ICMP协议。
ARP协议:用于获取一台电脑上的网卡号,即Mac地址。
RARP协议:根据Mac地址找IP。
这里写图片描述
使用arp -a可以查看Mac地址列表

3、路由器
连接多个逻辑上分开的网络。物理上至少有2个网卡。
这里写图片描述

Mac地址在两个设备之间通信时在变化;
IP地址在整个通信过程中都不会发生任何变化。
DNS底层用的是UDP协议。

ip:标记逻辑上的地址
mac:标记实际转发数据时的设备地址
netmask:和IP一起确定网络号
默认网关:发送的ip不在同一个网段内,那么会把数据转发给默认网关

TCP三次握手

这里写图片描述

TCP四次挥手

TCP数据请求完成后,客户端先调用close方法与服务器进行四次挥手关闭连接,避免占用资源。
这里写图片描述

TCP长连接与短连接

TCP在真正的读写操作之前,server与client之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,连接的建立通过三次握手,释放则需要四次握手,所以说每个连接的建立都是需要资源消耗和时间消耗的。
- 短连接:建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
- 长连接:建立连接——数据传输…(保持连接)…数据传输——关闭连接

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

TCP的十种状态

这里写图片描述

MSL:maximumsegment lifetime,报文段最大生存时间

网络攻击

tcp半链接攻击

tcp半链接攻击也称为:SYN Flood (SYN洪水)

是种典型的DoS (Denial of Service,拒绝服务) 攻击

效果就是服务器TCP连接资源耗尽,停止响应正常的TCP连接请求

dns攻击

dns服务器被劫持

我们知道一个域名服务器对其区域内的用户解析请求负责,

但是并没有一个机制去监督它有没有真地负责。

也就是说域名服务器的权力并没有被关在笼子里,

所以它既可以认真地“为人民服务”,也可以“指鹿为马”。

于是有些流氓的域名服务器故意更改一些域名的解析结果,

将用户引向一个错误的目标地址。这就叫作 DNS 劫持,主要用来阻止用户访问某些特定的网站,或者是将用户引导到广告页面。

dns欺骗

DNS 欺骗简单来说就是用一个假的 DNS 应答来欺骗用户计算机,

让其相信这个假的地址,并且抛弃真正的 DNS 应答。

在一台主机发出 DNS 请求后,它就开始等待应答,

如果此时有一个看起来正确(拥有和DNS请求一样的序列号)的应答包,

它就会信以为真,并且丢弃稍晚一点到达的应答。

查看域名解析的ip地址方法
nslookup 域名
例如:
nslookup baidu.com

arp攻击

并发服务器

单进程服务器

from socket import *

serSocket = socket(AF_INET, SOCK_STREAM)

# 重复使用绑定的信息
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)

localAddr = ('', 7788)

serSocket.bind(localAddr)

serSocket.listen(5)

while True:
    newSocket,destAddr = serSocket.accept()
    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()

多进程服务器

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()

多线程服务器

#coding=utf-8
from socket import *
from threading import Thread
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 = Thread(target=dealWithClient, args=(newSocket,destAddr))
            client.start()

            #因为线程中共享这个套接字,如果关闭了会导致这个套接字不可用,
            #但是此时在线程中这个套接字可能还在收数据,因此不能关闭
            #newSocket.close() 
    finally:
        serSocket.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:

        #用来测试
        #time.sleep(0.5)

        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()

TCP服务器-select版

在多路复用的模型中,比较常用的有select模型和epoll模型。这两个都是系统接口,由操作系统提供。当然,Python的select模块进行了更高级的封装。

import select
import socket
import sys


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

inputs = [server, sys.stdin]

running = True

while True:

    # 调用 select 函数,阻塞等待
    #第一个参数表示检测可以收数据的socket列表,第二个表示检测可以发数据的套接字列表,第三个参数表示检测产生异常的套接字列表
    readable, writeable, exceptional = select.select(inputs, [], [])

    # 数据抵达,循环
    for sock in readable:

        # 监听到有新的连接
        if sock == server:
            conn, addr = server.accept()
            # select 监听的socket
            inputs.append(conn)

        # 监听到键盘有输入
        elif sock == sys.stdin:
            cmd = sys.stdin.readline()
            running = False
            break

        # 有数据到达
        else:
            # 读取客户端连接发送的数据
            data = sock.recv(1024)
            if data:
                sock.send(data)
            else:
                # 移除select监听的socket
                inputs.remove(sock)
                sock.close()

    # 如果检测到用户输入敲击键盘,那么就退出
    if not running:
        break

server.close()
  • 优点
    select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

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

TCP服务器-epoll版

poll解决了套接字有上限的问题,但依旧使用轮询。epoll采用事件通知机制。

import socket
import select

# 创建套接字
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

# 绑定本机信息
s.bind(("",7788))

# 变为被动
s.listen(10)

# 创建一个epoll对象
epoll=select.epoll()

# 测试,用来打印套接字对应的文件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET

# 注册事件到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:

        # print fd
        # print events

        # 如果是socket创建的套接字被激活
        if fd == s.fileno():
            conn,addr=s.accept()

            print('有新的客户端到来%s'%str(addr))

            # 将 conn 和 addr 信息分别保存起来
            connections[conn.fileno()] = conn
            addresses[conn.fileno()] = addr

            # 向 epoll 中注册 连接 socket 的 可读 事件
            epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)


        elif events == select.EPOLLIN:
            # 从激活 fd 上接收
            recvData = connections[fd].recv(1024)

            if len(recvData)>0:
                print('recv:%s'%recvData)
            else:
                # 从 epoll 中移除该 连接 fd
                epoll.unregister(fd)

                # server 侧主动关闭该 连接 fd
                connections[fd].close()

                print("%s---offline---"%str(addresses[fd]))
  • EPOLLIN (可读)
  • EPOLLOUT (可写)
  • EPOLLET (ET模式)

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。
ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。

协程

微线程。相对于进程与线程,协程的切换对系统性能的消耗最小。特别是IO密集型任务使用协程效率最高,计算(CPU)密集型使用多进程最好。

简单的协程实现:

import time

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


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

if __name__ == '__main__':
    a = A()
    B(a)

协程-greenlet版

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单。使用如下命令安装greenlet模块:sudo pip install 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已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent

greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent.

import gevent

def f(n):
    for i in range(n):
        print gevent.getcurrent(), i

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

可以看到,3个gevent是依次运行而不是交替运行

gevent切换执行
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()
gevent并发下载器

当然,实际代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时,gevent自动切换,代码如下

#coding=utf-8

from gevent import monkey; 
import gevent
import urllib2

#有IO才做时需要这一句
monkey.patch_all()

def myDownLoad(url):
    print('GET: %s' % url)
    resp = urllib2.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(myDownLoad, 'http://www.baidu.com/'),
        gevent.spawn(myDownLoad, 'http://www.itcast.cn/'),
        gevent.spawn(myDownLoad, 'http://www.itheima.com/'),
])
gevent版-TCP服务器
import sys
import time
import gevent

from gevent import socket,monkey
#gevent实现协程自动切换前需要执行此句,对python代码进行了修改。
monkey.patch_all()

def handle_request(conn):
    while True:
        data = conn.recv(1024)
        if not data:
            conn.close()
            break
        print("recv:", data)
        conn.send(data)


def server(port):
    s = socket.socket()
    s.bind(('', port))
    s.listen(5)
    while True:
        cli, addr = s.accept()
        gevent.spawn(handle_request, cli)

if __name__ == '__main__':
    server(7788)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值