Web服务器

有个关于正则的注意点,不要随便加空格....

下面写的关于匹配邮箱的正则,原本很简单的一个题,被我在"{4,20}"之间加个空格就不行了。注意注意....

ret = re.match(r"[a-zA-Z0-9_]{4,20}@(163|qq|gmail)\.com$", 'hello@163.com').group()

 分组的用法,使标签前后保持一致

>>> html_str = "<h1>hahhahaha</h1>"
>>> re.match(r"<(\w*)>.*</\1>", html_str).group()
'<h1>hahhahaha</h1>'

 

>>> html_str = "<h1>hahhahaha</h2>"
>>> re.match(r"<(\w*)>.*</\1>", html_str).group()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'

 另一种方法:起别名

>>> re.match(r"<(?P<p1>\w*)>.*</(?P=p1)>", html_str).group()
'<h1>hahhahaha</h1>'

“?”的作用是匹配0次或者1次

ret = re.match("[1-9]","0").group()  # 报错

ret = re.match("[1-9]?","0").group()  #输出空字符串

提取区号和电话号码

>>> ret = re.match("([^-]*)-(\d+)","010-12345678")
>>> ret.group()
'010-12345678'
>>> ret.group(1)
'010'
>>> ret.group(2)
'12345678'

 

sub可以传递函数

 三次握手(相当于调用connect)

四次挥手(相当于调用close())

①客户端告诉服务器不会给它发送数据了(此时全双工客户端关了一个,还剩一个用来收数据的)(关掉客户端发送)

②(1)服务器收到消息通知应用程序"recv_data = new_socke.recv()"解堵塞。(关闭服务端接收)

    (2)回应客户端已收到

③(1)服务器执行new_socket.close()(关闭服务端发送)

    (2)发数据包通知客户端服务端也已关闭

④(1)客户端关闭接收

    (2)发消息告诉服务端已收到

为什么在四次挥手中是客户端先调用close()而不是服务端?因为谁先调用close(),谁最后就要多等两分钟(2MSL)。理由是这样的,首先客户端调用close()关闭自己的发送端,然后发消息通知服务端,服务端收到后关闭自己的接收端,返回一个收到的消息,此时客户端才真正释放"发送端口“这个资源。接下来服务端调用close()关闭自己的发送端,但是还没释放资源,它也是先发送一个通知给客户端,客户端收到后,就关闭自己的接收端,并返回一个收到的消息,此时也没有释放资源。为什么不立刻释放资源呢?是为了避免发送的包在传输过程中出现错误而不能到达对方,所以设定了超时重传,一旦超过了规定时间的话,就会重新发生数据包。所以当服务端想关闭发送端,并发送通知给客户端的时候,客户端要返回一个应答给服务端,如果这个包发送失败,服务端接收不到,超时(MSL)的话,会在重新发送一个,所以如果客户端在发送应答的数据包之后就释放资源,那么如果包发送失败了,服务端接受不到,再重新发来的包客户端就不知道,所以客户端需要先等2MSL时间,看看还有没有来自服务端的通知信息。

这也解释了前面所说的,为什么要客户端先调用close()。因为服务端是绑定端口的,客户端不绑定。谁先调用close(),谁就要多等2MSL的时间,那么如果是服务端先调用close(),会占用服务端的端口,而因为客户端端口是随机绑定的,因此即使被占用,重新连接还有别的端口。

 


 

多进程实现http服务器

在开启多进程后,为什么还要再多调用一个new_socket.close()(蓝框那里)?

因为开多一个子进程,意味着多复制了一个资源在运行程序。同时,在linux中,一切内容皆文件。当有用户连进来时,本来主进程创建的new_socket对象,在linux底层对应一个文件描述符(fd)也就是数字,该文件描述符对应一个特殊文件,这里就是网络接口。但是当你开多一个进程,意味复制了一份new_socket,此时相当于有两个new_socket对象同时指向该文件描述符,有点像linux中硬链接的感觉,因此只关闭一个new_socket是不够的,后面四次挥手不能进行。所以需要在刚创建一个子进程后,先把主进程的new_socket给关了。

开多线程就不用这样,线程是全局资源共享,共用一个套接字。

单进程、单线程、非堵塞实现并发的原理

类似协程gevent的工作原理(要求程序尽可能快的处理发来的保存在电脑缓存中的信息,不然会使电脑卡)

 

import socket


def main():
    tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server.bind("", 77888)
    tcp_server.listen(128)
    tcp_server.setblocking(False)  # 设置为非堵塞
    client_socket_list = list()
    while True:
        try:
            new_socket, new_addr = tcp_server.accept()
        except Exception as f:
            print("------没有新的客户端到来------")
        else:
            print("------只要没有产生异常,意味着来了一个新的客户------")
            new_socket.setblocking(False)
            client_socket_list.append(new_socket)

        for client_socket in client_socket_list:
            try:
                client_data = client_socket.recv(1024)  # 接收到数据有两种情况,一是用户发送来数据;二是用户调用close()
            except Exception as f:
                print("-----产生异常,说明这个客户端还没有消息传来-----")
            else:
                if client_data:
                    # 对方发送过来数据
                    print("-----客户端发来了数据-----")
                else:
                    # 对方调用close(),导致 recv返回
                    client_socket_list.remove(client_socket)
                    client_socket.close()


if __name__ == "__main__":
    main()

之前单进程单线程不能实现并发是因为套接字会堵塞,无论是在调用accept()等待用户接入还是调用recv()等待用户发送消息,都会造成套接字堵塞,因此只能一个一个用户服务。我们的解决办法就是给套接字解堵塞,再使用异常判断,如果没有用户接入或者没有用户发消息过来时就抛弃异常循环监听,一旦用户接入进来,就把产生的套接字放到一个列表里面然后再循环读出。

 

实现“单进程-单线程-长链接”的过程。有一个点需要注意就是为了实现时时链接,在关闭了套接字的close()之后,要在responce_header里面加上responce_body的长度,不然浏览器在读取服务器发送过来的数据时不会结束,会一直继续刷新等待服务器的发送,此时客户端无法继续向服务器发送请求。因此添加responce_body的长度后,浏览器根据长度截取获取到的信息,而后客户端可继续发送请求。

import time
import socket
import sys
import re


class WSGIServer(object):
    """定义一个WSGI服务器的类"""

    def __init__(self, port, documents_root):

        # 1. 创建套接字
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2. 绑定本地信息
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_socket.bind(("", port))
        # 3. 变为监听套接字
        self.server_socket.listen(128)

        self.server_socket.setblocking(False)
        self.client_socket_list = list()

        self.documents_root = documents_root

    def run_forever(self):
        """运行服务器"""

        # 等待对方链接
        while True:

            # time.sleep(0.5)  # for test

            try:
                new_socket, new_addr = self.server_socket.accept()
            except Exception as ret:
                print("-----1----", ret)  # for test
            else:
                new_socket.setblocking(False)
                self.client_socket_list.append(new_socket)

            for client_socket in self.client_socket_list:
                try:
                    request = client_socket.recv(1024).decode('utf-8')
                except Exception as ret:
                    print("------2----", ret)  # for test
                else:
                    if request:
                        self.deal_with_request(request, client_socket)
                    else:
                        client_socket.close()
                        self.client_socket_list.remove(client_socket)

            print(self.client_socket_list)


    def deal_with_request(self, request, client_socket):
        """为这个浏览器服务器"""
        if not request:
            return

        request_lines = request.splitlines()
        for i, line in enumerate(request_lines):
            print(i, line)

        # 提取请求的文件(index.html)
        # GET /a/b/c/d/e/index.html HTTP/1.1
        ret = re.match(r"([^/]*)([^ ]+)", request_lines[0])
        if ret:
            print("正则提取数据:", ret.group(1))
            print("正则提取数据:", ret.group(2))
            file_name = ret.group(2)
            if file_name == "/":
                file_name = "/index.html"


        # 读取文件数据
        try:
            f = open(self.documents_root+file_name, "rb")
        except:
            response_body = "file not found, 请输入正确的url"
            response_header = "HTTP/1.1 404 not found\r\n"
            response_header += "Content-Type: text/html; charset=utf-8\r\n"
            response_header += "Content-Length: %d\r\n" % (len(response_body))
            response_header += "\r\n"

            # 将header返回给浏览器
            client_socket.send(response_header.encode('utf-8'))

            # 将body返回给浏览器
            client_socket.send(response_body.encode("utf-8"))
        else:
            content = f.read()
            f.close()

            response_body = content
            response_header = "HTTP/1.1 200 OK\r\n"
            response_header += "Content-Length: %d\r\n" % (len(response_body))
            response_header += "\r\n"

            # 将header返回给浏览器
            client_socket.send( response_header.encode('utf-8') + response_body)


# 设置服务器服务静态资源时的路径
DOCUMENTS_ROOT = "./html"


def main():
    """控制web服务器整体"""
    # python3 xxxx.py 7890
    if len(sys.argv) == 2:
        port = sys.argv[1]
        if port.isdigit():
            port = int(port)
    else:
        print("运行方式如: python3 xxx.py 7890")
        return

    print("http服务器使用的port:%s" % port)
    http_server = WSGIServer(port, DOCUMENTS_ROOT)
    http_server.run_forever()


if __name__ == "__main__":
    main()
View Code

 

epoll过程了解

上面我们写的单进程-单线程-非堵塞并发的代码如果遇到同一时间很多人访问的时候,那么列表里面就会有很多套接字,这时一个一个循环遍历检查的效率就会显得很低,这种方式称为轮询。第二个影响效率的原因就是我们创建一个列表,再往里面添加套接字,相当于应用程序在内存中单独开辟一块空间,每次轮询就是把一个套接字对应的fd(文件描述符)复制一份,送到内核态里面,交给操作系统进行检查。

而epoll就主要解决了这两种问题。首先是它开辟的内存空间用来存放套接字的地方,是直接和内核态(kernel)共用的,因此省去了复制一份套接字(fd)的过程还有取出套接字(fd)送到内核态中的过程;另一个就是它采用事件通知的方式,不一个一个进行检查询问,而是对内存中的套接字所对应的文件描述符(fd)采用事件通知,什么时候收到数据,查看是属于哪个套接字,然后操作系统通知应用程序哪个套接字可以接收了,从而达到提升效率的目的。

epoll的作用就是把原本需要应用程序去遍历列表的过程交给操作系统去做。

 

import socket
import select


def server_client():
    # 进行数据的收发等
    pass


def main():
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.bind("", 8080)
    tcp_server_socket.listen(128)
    tcp_server_socket.setblocking(False)

    # 创建一个epoll对象
    epl = select.epoll()
    # 将监听套接字对应的fd注册到epoll中
    epl.register(tcp_server_socket.fileno(), select.EPOLLIN)
    # 创建用于存放fd和套接字对应关系的字典
    fd_event_dict = dict()
    while True:
        fd_event_list = epl.poll()  # 默认会堵塞,知道os监测到数据到来,通过事件通知的方式告诉这个程序,此时才会解堵塞
        # [(fd, event), (返回的列表里面是一个个元组  套接字对应的文件描述符,这个文件描述符到底是什么事件,例如可以调用recv接收等)]
        for fd, event in fd_event_list:
            # 等待新客户的链接
            if fd == tcp_server_socket.fileno():
                new_socket, client_addr = tcp_server_socket.accept()
                epl.register(new_socket.fileno(), select.EPOLLIN)
                fd_event_dict[new_socket.fileno()] = new_socket
            elif event == select.EPOLLIN:
                # 判断已链接的客户端是否有数据发送过来
                recv_data = fd_event_dict[fd].recv(1024).decode("utf-8")
                if recv_data:
                    server_client(fd_event_dict[fd], recv_data)
                else:
                    fd_event_dict[fd].cloes()
                    epl.unregister(fd)
                    del fd_event_dict[fd]

 


 

一个应用程序以TCP协议正在使用一个端口例如8080,那么就不允许被其他使用TCP协议的应用程序使用8080,但是此时使用其他协议例如UDP的程序就可以也使用8080。

子网掩码的作用就是确定IP地址哪部分属于网络号哪部分属于主机号。

集线器的通信方式是广播,这样会导致占用线路,影响别的用户通信。后来改成交换机,特点就是可以广播,也可以单播。

实际地址,也就是网卡地址(mac地址),前三组数字表示生产产家,后三组数字是实际生产的网卡编码。

路由器的作用是连接两个网络,一个网络内的电脑要想发数据给另一个网络内的,必须设定网关,网关一般就是路由器。发数据的时候,目的mac地址写的是网关的mac地址,经过网关处理后,再改成目的电脑的mac地址。因此整个过程一直在变的是mac地址,目的IP不变。

为什么有了IP地址还要有mac地址。因为IP地址是在逻辑层面标注的数据要送到哪里,但是实际一层一层之间的传递,是靠的mac地址。

https://www.zhihu.com/question/21546408

从浏览器发起一次请求到http服务器的过程:

首先检查是否有默认网关的mac地址,如果没有,就用广播的方式获取网关的mac地址,然后根据电脑配置的DNS服务器的IP,带上要访问的域名向DNS服务器查询域名的IP,经过网关、路由器等到达后,DNS解析获取Ip,返回到客户端,然后客户端根据这个IP再经过网关等一系列的传播,和http服务器三次握手成功,传输数据,最后再四次挥手结束。

 

posted @ 2018-11-29 00:36  心灵蚂蚁  阅读(314)  评论(0编辑  收藏  举报