Python高级 -- 05 网络(http协议、web服务器、tcp/ip协议、网络通信)

一、HTTP协议、web静态服务器



1、实现返回固定页面的简单的http服务器



import socket


def server_client(new_socket):
    """为请求的客户端进行服务的方法"""

    # 1.接收客户端发送的请求,即http请求
    request = new_socket.recv(1024)
    print("接收到的请求数据是:",request)

    # 2.准备返回给浏览器的http数据
        # 2.1 准备响应头信息
    response = "HTTP/1.1 200 OK\r\n"    # 浏览器中正规的换行符号是:\r\n
        # 2.2 准备响应空行
    response += "\r\n"
        # 2.3 准备响应体数据
    response += "copy that, i will response that"

    # 3.发送数据,并设置数据格式(浏览器解析数据的格式)
    new_socket.send(response.encode("utf-8"))

    # 4.关闭套接字
    new_socket.close()


def main():
    # 1.创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定8080端口
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2.绑定端口和ip
    tcp_server_socket.bind(("", 8080))

    # 3.把套接字设置为监听状态
    tcp_server_socket.listen(128)

    # 4.等待客户端的链接
    while True:
        new_socket, client_addr = tcp_server_socket.accept()

        # 5.链接客户端后,调用方法,对客户端进行服务
        server_client(new_socket)

    # 关闭服务端套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()





2、返回指定页面的数据的http服务器



(1)、导入提前准备好的html压缩包



http://download.csdn.net/download/wingzhezhe/10238240



(2)、上传压缩包到ubuntu系统与代码同一个目录中






(3)、编写代码



import socket
import re


def service_client(new_socket):
    """处理客户端发送的请求,并返回指定的数据"""

    # 1.接收客户端发送的http请求,并以utf-8的方式解码
    request = new_socket.recv(1024).decode("utf-8")
    print("-----------------服务器接收到的原始http数据  start   ----------------------")
    print(request)
    print("-----------------服务器接收到的原始http数据  end   ----------------------")

    # 2.处理接收到的数据
    #   2.1 使用 str.splitlines() 方法,将接收到的数据str以行为单位分割,放入一个list列表中。
    request_list = request.splitlines()

    print("-----------------服务器数据以行分割之后的数据  start   ----------------------")
    print(request_list)
    print("-----------------服务器数据以行分割之后的数据  end   ----------------------")

    # 3.获取要访问的具体资源名称
    file_name = ""
    #   3.1 使用正则表达式获取访问的资源名
    ret = re.match(r"[^/]+(/[^ ]*)", request_list[0])
    #   3.2 判断文件名是否为None
    if ret:
        # 不是None,获取第一个分组中的数据
        file_name = ret.group(1)

        # 如果获取到的文件名为 / 默认指定为index.html
        if file_name == "/":
            file_name = "/index.html"

    """
        根据获取到的文件名读取文件内容,并组装成http格式返回给浏览器
        使用try...except...else... 包裹起来,为了让程序在读到file_name=""的情况下,也能正常执行
    """
    try:
        f = open("./html" + file_name, "rb")
    except:
        # 没有找到资源,打开文件失败,就返回给页面404
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "SORRY THE FILE YOU FOUND IS NOT EXIST, PLEASE TRY AGAIN"
        new_socket.send(response.encode("utf-8"))
    else:
        # 读取文件没有发生异常,将读到的内容返回给浏览器
        file_content = f.read()
        f.close()
        # 设置响应头信息
        response = "HTTP/1.1 200 OK\r\n"
        response += "\r\n"
        # 将响应头发送给浏览器
        new_socket.send(response.encode("utf-8"))
        # 将响应体(读取到的文件内容)发送给浏览器
        new_socket.send(file_content)

    # 4.读取并发送数据完毕,关闭套接字
    new_socket.close()


def main():
    # 1.创建socket套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2.绑定ip和port
    tcp_server_socket.bind(("", 8080))

    # 3.设置socket为监听状态
    tcp_server_socket.listen(128)

    # 4.开始接收
    while True:
        # 等待客户端链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5.为客户端服务
        service_client(new_socket)

    # 6.关闭套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()




(4)、ubuntu测试







3、使用多进程实现返回指定页面的数据的http服务器



import socket
import re
import multiprocessing


def service_client(new_socket):
    """处理客户端发送的请求"""

    # 1.接收客户端发送的请求
    request = new_socket.recv(1024).decode("utf-8")
    print(" " * 50)
    print(request)
    print(" " * 50)

    # 2.处理接收到的数据
    request_list = request.splitlines()

    # 3.获取要访问的资源名称
    file_name = ""
    ret = re.match(r"[^/]+(/[^ ]*)", request_list[0])
    if ret:
        file_name = ret.group(1)

        if file_name == "/":
            file_name = "/index.html"

    # 4.读取数据,并发送给浏览器数据
    try:
        f = open("./html" + file_name, "rb")
    except:
        response = "HTTP/1.1 404 FILE NOT FOUNT\r\n"
        response += "\r\n"
        response += "SORRY THIE FILE YOU FOUND IS NOT EXIS"
        new_socket.send(response.encode("utf-8"))
    else:
        file_content = f.read()
        f.close()
        response = "HTTP/1.1 200 OK\r\n"
        response += "\r\n"
        new_socket.send(response.encode("utf-8"))
        new_socket.send(file_content)

    # 5.关闭套接字
    new_socket.close()



def main():
    # 1.创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2.绑定ip和端口号
    tcp_server_socket.bind(("", 8080))

    # 3.设置socket为监听
    tcp_server_socket.listen(128)

    # 4.开始接收
    while True:
        # 等待客户端链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5.使用多进程为客户端服务
        p = multiprocessing.Process(target = service_client, args = (new_socket, ))
        p.start()

        """
            ⭐⭐⭐注意:此处需要单独对 new_socket 进行close()操作
                  多进程中,子进程执行时会对主进程中的内容copy一份来执行,因此,子进程
                  中的 new_socket 在子进程中close之后,主进程创建出来的new_socket并没有
                  close,需要在主进程中再次进行close()操作
        """
        new_socket.close()

    # 6.关闭套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()


思考:使用多任务有什么好处呢?


        使用多任务的时候,可以让服务器同时为多个用户提供服务。


思考:使用多任务的时候,每个用户访问都需要消耗一定的内存资源,如果用户访问量过大,导致服务器占用内存过高,内存承受不了应该如果解决?


        使用gevent来进行多任务访问服务器



4、使用gevent实现多任务服务器



import socket
import re
import gevent
from gevent import monkey


# 给延时操作打补丁
monkey.patch_all()


def service_client(new_socket):
    """处理客户端发送的请求,并返回指定的数据"""

    # 1.接收客户端发送的http请求,并以utf-8的方式解码
    request = new_socket.recv(1024).decode("utf-8")
    print("-----------------服务器接收到的原始http数据  start   ----------------------")
    print(request)
    print("-----------------服务器接收到的原始http数据  end   ----------------------")

    # 2.处理接收到的数据
    #   2.1 使用 str.splitlines() 方法,将接收到的数据str以行为单位分割,放入一个list列表中。
    request_list = request.splitlines()

    print("-----------------服务器数据以行分割之后的数据  start   ----------------------")
    print(request_list)
    print("-----------------服务器数据以行分割之后的数据  end   ----------------------")

    # 3.获取要访问的具体资源名称
    file_name = ""
    #   3.1 使用正则表达式获取访问的资源名
    ret = re.match(r"[^/]+(/[^ ]*)", request_list[0])
    #   3.2 判断文件名是否为None
    if ret:
        # 不是None,获取第一个分组中的数据
        file_name = ret.group(1)

        # 如果获取到的文件名为 / 默认指定为index.html
        if file_name == "/":
            file_name = "/index.html"

    """
        根据获取到的文件名读取文件内容,并组装成http格式返回给浏览器
        使用try...except...else... 包裹起来,为了让程序在读到file_name=""的情况下,也能正常执行
    """
    try:
        f = open("./html" + file_name, "rb")
    except:
        # 没有找到资源,打开文件失败,就返回给页面404
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "SORRY THE FILE YOU FOUND IS NOT EXIST, PLEASE TRY AGAIN"
        new_socket.send(response.encode("utf-8"))
    else:
        # 读取文件没有发生异常,将读到的内容返回给浏览器
        file_content = f.read()
        f.close()
        # 设置响应头信息
        response = "HTTP/1.1 200 OK\r\n"
        response += "\r\n"
        # 将响应头发送给浏览器
        new_socket.send(response.encode("utf-8"))
        # 将响应体(读取到的文件内容)发送给浏览器
        new_socket.send(file_content)

    # 4.读取并发送数据完毕,关闭套接字
    new_socket.close()


def main():
    # 1.创建socket套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2.绑定ip和port
    tcp_server_socket.bind(("", 8080))

    # 3.设置socket为监听状态
    tcp_server_socket.listen(128)

    # 4.开始接收
    while True:
        # 等待客户端链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5.使用协程(gevent)为客户端服务⭐⭐⭐
        gevent.spawn(service_client, new_socket)

    # 6.关闭套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()


思考:使用gevent其实是实现了单线程阻塞状态下的所有请求一起执行,但是如何才能实现单线程单进程非阻塞状态下的多任务请求呢?


        请看 5



5、使用单线程单进程非阻塞状态实现多任务的htt服务器



import socket
import time


# 1.创建socket
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 2.绑定端口和ip
tcp_server_socket.bind(("", 8080))

# 3.设置socket为监听状态
tcp_server_socket.listen(128)

# 4.设置套接字为非阻塞状态
tcp_server_socket.setblocking(False)

# 5.创建列表,用于存储每次接收到客户端请求之后创建的客户端套接字对象
client_socket_list = list()

"""
    使用无限循环来保证每次都能接收到客户端的请求
"""
while True:

    time.sleep(0.5)
    try:
        # 当服务器每次循环的时候都会监听是否有客户端请求,如果没有,抛出异常
        new_socket, client_addr = tcp_server_socket.accept()
    except Exception as e:
        # 没有客户端请求
        print("--------------没有客户端到来--------------")
    else:
        # 如果没有发生异常,则表明当前循环接收到了客户端请求
        # 将创建出来的客户端套接字设置为非阻塞状态,目的是如果当前客户端没有发送数据,则还可以继续执行循环
        new_socket.setblocking(False)
        # 将创建出来的客户端套接字添加到列表中
        client_socket_list.append(new_socket)

    # 监听是否有客户端到来的操作执行完成之后,遍历客户端socket列表,执行接收客户端消息的操作
    for client_socket in client_socket_list:
        try:
            # 使用客户端套接字接收数据,如果有数据,则不抛出异常
            recv_data = client_socket.recv(1024)
        except Exception as e:
            # 如果发生异常,证明当前的客户端套接字没有接收到客户端发送的数据
            print("-------------客户端没有发送数据-----------")
        else:
            # 没有发生异常,则表明当前循环接收到遍历出来的客户端套接字发送的数据
            print(recv_data)
            # 判断接收的数据是否为空,如果是空,证明客户端已经执行close操作,服务器也需要对相应的客户端套接字执行close操作
            if recv_data:
                print("------客户端发送了数据------")
            else:
                # 客户端调用了close方法,导致了recv没有数据
                client_socket.close()
                client_socket_list.remove(client_socket)
                print("-------------客户端已经执行了关闭的操作-------------")



6、长连接的方式实现http服务器



什么是长链接?

        短连接:客户端每次请求服务器的时候,都需要进行三次握手,而服务器每次接收到客户端的请求,也会创建一个客户端对象,在使用完客户端对象后,对客户端对象进行close( )操作的方式成为短连接,即服务器每次接收完客户端数据之后立即执行close()操作。


        长连接:服务端接收到客户端请求之后,只创建一次客户端套接字对象,等客户端再次进行发送数据的时候,服务端重复利用之前创建的客户端套接字对象进行数据的收发操作,这种方式称为长连接。


使用长连接、单线程、单进程、非阻塞方式实现http服务器代码: 


import socket
import re


def service_client(new_socket, request):
    """处理客户端发送的请求,并返回指定的数据"""

    # 1.接收客户端发送的http请求,并以utf-8的方式解码
    # request = new_socket.recv(1024).decode("utf-8")
    print("-----------------服务器接收到的原始http数据  start   ----------------------")
    print(request)
    print("-----------------服务器接收到的原始http数据  end   ----------------------")

    # 2.处理接收到的数据
    #   2.1 使用 str.splitlines() 方法,将接收到的数据str以行为单位分割,放入一个list列表中。
    request_list = request.splitlines()

    print("-----------------服务器数据以行分割之后的数据  start   ----------------------")
    print(request_list)
    print("-----------------服务器数据以行分割之后的数据  end   ----------------------")
    
    # 3.获取要访问的具体资源名称
    file_name = ""
    #   3.1 使用正则表达式获取访问的资源名
    ret = re.match(r"[^/]+(/[^ ]*)", request_list[0])
    #   3.2 判断文件名是否为None
    if ret:
        # 不是None,获取第一个分组中的数据
        file_name = ret.group(1)
        
        # 如果获取到的文件名为 / 默认指定为index.html
        if file_name == "/":
            file_name = "/index.html"

    """
        根据获取到的文件名读取文件内容,并组装成http格式返回给浏览器
        使用try...except...else... 包裹起来,为了让程序在读到file_name=""的情况下,也能正常执行
    """
    try:
        f = open("./html" + file_name, "rb")
    except:
        # 没有找到资源,打开文件失败,就返回给页面404
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "SORRY THE FILE YOU FOUND IS NOT EXIST, PLEASE TRY AGAIN"
        new_socket.send(response.encode("utf-8"))
    else:
        # 读取文件没有发生异常,将读到的内容返回给浏览器
        # 设置响应体内容
        response_body = f.read()

        f.close()

        # 设置响应头信息
        response_head = "HTTP/1.1 200 OK\r\n"
        """  
            设置 Content-Length属性是为了配合长连接使用,当客户端浏览器收到此属性之后,
            会自动根据Content-Length属性的值去读取response_body中的数据内容,读取完毕之后,
            便将读到的内容展示到浏览器页面,并等待下次的请求⭐⭐⭐
        """
        response_head += "Content-Length:%d\r\n" % len(response_body)
        response_head += "\r\n"

        # 将响应信息发给浏览器
        # 字符串和二进制之间不能进行+的操作,需要将response_head以utf-8的编码进行转换成为二进制,再和读取出来的二进制文件进行+的操作⭐⭐⭐
        response = response_head.encode("utf-8") + response_body

        new_socket.send(response)

        # 4.读取并发送数据完毕,目前使用的是长链接,就不需要关闭套接字了
    
        # new_socket.close()


def main():
    # 1.创建socket套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2.绑定ip和port
    tcp_server_socket.bind(("", 8080))

    # 3.设置socket为监听状态
    tcp_server_socket.listen(128)

    # 4.设置socket为非阻塞状态
    tcp_server_socket.setblocking(False)

    # 创建list列表,用来存储每次客户端访问服务器时创建的客户端套接字对象
    client_socket_list = list()

    # 5.开始接收⭐⭐⭐
    while True:
        # 等待客户端链接
        try:
            new_socket, client_addr = tcp_server_socket.accept()
        except Exception as e:
            # 非阻塞状态下的套接字会接收客户端数据,并创建对象,如果客户端没有链接请求,就会抛出异常
            pass
        else:
            # 设置客户端套接字对象为非阻塞状态
            new_socket.setblocking(False)
            # 将创建的客户端套接字对象放入列表中
            client_socket_list.append(new_socket)

        # 遍历列表中的客户端socket对象,接收信息
        for client_socket in client_socket_list:
            try:
                # 6.为客户端服务
                recv_data = client_socket.recv(1024).decode("utf-8")
            except Exception as e:
                pass
            else:
                if recv_data:
                    # 如果接收到的数据不是None,调用方法,为客户端服务
                    service_client(client_socket, recv_data)
                else:
                    # 客户端已经调用了close()方法,将客户端套接字对象从列表中移除并close⭐⭐⭐
                    client_socket.close()
                    client_socket_list.remove(client_socket)


    # 7.关闭套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()



二、epoll(重点、难点)



1、什么是epoll



详请参考:http://blog.csdn.net/xiajun07061225/article/details/9250579



2、使用epoll实现单线程、单进程、非阻塞方式的http服务器



import socket
import re
# 使用epoll需要导入select模块
import select


def service_client(new_socket, request):
    """处理客户端发送的请求,并返回指定的数据"""
    
    # 1.接收客户端发送的http请求,并以utf-8的方式解码
    # request = new_socket.recv(1024).decode("utf-8")
    print("-----------------服务器接收到的原始http数据  start   ----------------------")
    print(request)
    print("-----------------服务器接收到的原始http数据  end   ----------------------")
    
    # 2.处理接收到的数据
    #   2.1 使用 str.splitlines() 方法,将接收到的数据str以行为单位分割,放入一个list列表中。
    request_list = request.splitlines()
    
    print("-----------------服务器数据以行分割之后的数据  start   ----------------------")
    print(request_list)
    print("-----------------服务器数据以行分割之后的数据  end   ----------------------")
    
    # 3.获取要访问的具体资源名称
    file_name = ""
    #   3.1 使用正则表达式获取访问的资源名
    ret = re.match(r"[^/]+(/[^ ]*)", request_list[0])
    #   3.2 判断文件名是否为None
    if ret:
        # 不是None,获取第一个分组中的数据
        file_name = ret.group(1)
        
        # 如果获取到的文件名为 / 默认指定为index.html
        if file_name == "/":
            file_name = "/index.html"
    
    """
        根据获取到的文件名读取文件内容,并组装成http格式返回给浏览器
        使用try...except...else... 包裹起来,为了让程序在读到file_name=""的情况下,也能正常执行
    """         
    try:        
        f = open("./html" + file_name, "rb")
    except:     
        # 没有找到资源,打开文件失败,就返回给页面404
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "SORRY THE FILE YOU FOUND IS NOT EXIST, PLEASE TRY AGAIN"
        new_socket.send(response.encode("utf-8"))
    else:       
        # 读取文件没有发生异常,将读到的内容返回给浏览器
        response_body = f.read()
        f.close()
        # 设置响应头信息
        response_head = "HTTP/1.1 200 OK\r\n"
        response_head += "Content-Length:%d\r\n" % len(response_body)
        response_head += "\r\n"
                
        response = response_head.encode("utf-8") + response_body
        # 将响应头发送给浏览器
        new_socket.send(response)
                
    # 4.读取并发送数据完毕,如果是长连接,不需要关闭套接字
    # new_socket.close()
                
                
def main():     
    # 1.创建socket套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                
    # 2.绑定ip和port
    tcp_server_socket.bind(("", 8080))
                   
    # 3.设置socket为监听状态
    tcp_server_socket.listen(128)
                   
    # 设置服务端套接字为非阻塞状态
    tcp_server_socket.setblocking(False)
                   
    # 4.创建一个epoll对象
    epl = select.epoll()
                   
    # 5.将服务端监听套接字对应的文件描述符(fb)注册到epoll中
    # EPOLLIN (可读)
    # EPOLLOUT (可写)
    # EPOLLET (ET模式)
    epl.register(tcp_server_socket.fileno(), select.EPOLLIN)
                   
    # 创建一个字典,用来存放放入epoll对象中的套接字文件描述符和套接字对象
    fd_event_dict = dict()  # key : 文件描述符    value : 套接字对象
                   
    # 6.开始接收   
    while True:    
                   
        """        
			epoll对象默认会阻塞,直到操作系统(os)监测到有数据到来,
			os 会通过事件通知的方式告诉这个程序,这个时候,epoll才会解阻塞,
			并且epoll对象会调用 poll() 方法,获取epoll对象中注册的所有套接字的文件描述符,
			返回值是一个列表,存放的是对应需要应答的套接字对象的文件描述符和需要执行的事件
        """        
        fd_event_list = epl.poll()
                   
        # 遍历返回的列表
        for fd, event in fd_event_list:
            # 判断遍历的套接字文件描述符是否是服务端套接字文件描述符
            if fd == tcp_server_socket.fileno():
                # 如果是服务端套接字文件描述符,则说明有新的链接,需要创建新的客户端套接字
                   
                # 新的客户端链接
                new_socket, client_addr = tcp_server_socket.accept()
                # 将新产生的客户端套接字对象的文件描述符注册到epoll对象中
                epl.register(new_socket.fileno(), select.EPOLLIN)
                # 将新产生的客户端套接字对象和文件描述符放入字典对象中
                fd_event_dict[new_socket.fileno()] = new_socket
            elif event == select.EPOLLIN:
                # 如果是写入操作,即:有客户端数据发送过来,获取数据,并判断数据是否为None
                recv_data = fd_event_dict[fd].recv(1024).decode("utf-8")
                if recv_data:
                    # 如果有数据,就调用方法,处理请求数据
                    service_client(fd_event_dict[fd], recv_data)
                else:
                    # 如果没有数据,即:客户端调用了close方法发送的请求,服务端需要关闭对应的客户端套接字对象
                    fd_event_dict[fd].close()
                    # 移除epoll中的套接字对象
                    epl.unregister(fd)
                    # 删除字典中的文件描述符
                    del fd_event_dict[fd]
        
    # 7.关闭套接字
    tcp_server_socket.close()
    
    
if __name__ == "__main__":
    main()



猜你喜欢

转载自blog.csdn.net/wingzhezhe/article/details/79245728