参考here 参考刘江python课程

1. 什么是网络编程

网络编程就是如何实现两台计算机之间的网络通信。举个例子,当你使用浏览器访问新浪网时,你的计算机就和新浪的某台服务器通过互联网连接起来了,然后,新浪的服务器把网页内容作为数据通过互联网传输到你的电脑上。

由于你的电脑上可能不止浏览器,还有QQ、Skype、Dropbox、邮件客户端等,不同的程序连接的别的计算机也会不同,所以,更确切地说,网络通信是两台计算机上的两个进程之间的通信。

2. TCP/IP协议

互联网包含了上百种协议标准,其中最重要的是TCP/IP协议,因此大家也把互联网的协议简称为TCP/IP协议。

2.1. IP协议

IP协议负责把数据从一台计算机发送到另一台计算机上。数据被分割为一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此路由器就负责如何把一个IP包发送出去。IP包的特点是按块转发,途径多个路由,但不能保证到达,也不能保证顺序到达。

2.2. TCP协议

TCP协议是建立在IP协议之上的,TCP协议负责在两台计算机之间建立可靠连接,保证顺序按序到达。TCP协议会按照握手建立连接,然后对每个IP包编号,保证IP包按序到达,如果包丢掉了,就自动重发。

许多常用的更高级协议都是建立在TCP协议上的,比如HTTP协议,SMTP协议等。

一个TCP报文除了包含源数据之外,还包括源IP地址,目标IP地址,源端口和目标端口。

端口的作用:在两台计算机通信时,只发IP地址是不够的,因为一台计算机上运行着多个网络程序。一个TCP报文来了之后,到底是交给浏览器还是QQ,就需要端口号来区分。每个网络程序都向操作系统申请唯一的端口号,两个进程在计算机之间建立网络连接就需要各自的IP地址和各自的端口号。

一个进程可能同时与多台计算机建立连接,因此它会申请很多个端口。

3. Socket编程

Socket是网络编程的一个抽象概念,通常我们用一个socket表示打开一个网络连接,而打开一个Socket需要知道目标计算机的IP地址和端口号,接着指定协议类型即可。

3.1. 客户端

创建一个基于TCP连接的Socket。主要可以包含以下步骤:创建socket(打开网络连接)->建立连接(IP地址和端口号)->发送请求(请求网页的话需要遵循HTTP协议)->接受数据。

import socket

# 创建一个socket对象,socket.AF_INET表示因特网IPv4地址簇,socket.SOCK_STREAM表示使用TCP协议的socket。
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 要连接的服务器的IP地址和端口号,使用Baidu的域名可以直接转化到IP地址
# 由于要访问网页,80端口是web服务的标准端口。
s.connect(('www.baidu.com', 80))

# 发送请求,需要注意遵循http协议
# 但是请求头中需要包含哪些信息才能请求成功,具体要看网站,不同网站请求头不同
# 发送的时候发送的是bytes,因此需要进行编码
request = "GET / HTTP/1.1\r\nHost:www.baidu.com\r\nConnection:close\r\n\r\n"
s.send(request.encode('utf-8'))

# 接受服务器返回数据
buffer = []
while True:
    d = s.recv(1024)   # 每次最多接受1k字节
    if d:
        buffer.append(d)
    else:
        break

data = b"".join(buffer)
data = data.decode('utf-8')
print(data)

# 分割接受到的数据
# 接收到的数据包括HTTP头信息和网页本身,我们可以将这两部分信息分隔开然后保存网页到文件就可以了
header,html = data.split('\r\n\r\n', 1)
print(header)
with open('baidu.html', 'w', encoding="utf-8") as f:
    f.write(html)

3.2. 服务器端

和客户端相比,服务器端要复杂一些。首先,服务器进程需要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器与该客户端建立socket连接,随后的通信就靠这个socket连接。

但是服务器端要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者线程来处理,否则服务器一次就只能服务一个客户端。

bind:绑定要监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以者使用127.0.0.1绑定到本机地址,如果绑定到本机地址,客户端必须同时在本机运行才能连接,也就是说外部的计算机无法连接进来。如果bind绑定的ip地址字符串为空(等价于0.0.0.0),那么将接受本机所有可用的IPv4地址。

  • 通过在命令行输入netstat -an可以查看当前的TCP/IP网络连接状态,通过

      addrs = socket.getaddrinfo(socket.gethostname(),None)

可以查看本机的所有IPv4和IPv6地址,比如我首先绑定了0.0.0.0:65432,然后在客户端通过192.168.0.110:65432就可以成功访问服务器。

  • 如果绑定的是127.0.0.1:65432,那么客户端只能通过127.0.0.1:65432来访问。
  • 对于本机没有的IP地址,是不可以绑定的。比如我想绑定196.168.0.111,此时就会报错 OSError: [WinError 10049] 在其上下文中,该请求的地址无效。
import socket
import threading
import time

# 创建一个socket对象,socket.AF_INET表示因特网IPv4地址簇,socket.SOCK_STREAM表示使用TCP协议的socket。
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)


# 创建主线程的socket,并绑定端口,bind() 用来关联 socket 到指定的网络接口(IP 地址)和端口号,0.0.0.0表示
server.bind(('127.0.0.1', 8000))
server.listen()


# 处理接受到的请求
def handle_sock(sock, addr):
    print("接收到来自 %s:%s 的请求" % addr)
    sock.send(b'welcome')

    while True:
        # 获取从客户端发送的数据,一次获取1kb的数据。
        data = sock.recv(1024)  # 使用sock,因为sock是建立连接之后返回的socket,用于客户端和服务器端传输数据
        time.sleep(1)
        print(data.decode('utf-8'))
        # 如果发送空的数据,就表示要关闭这个连接。
        if not data or data.decode('utf-8') == 'exit':
            break

        sock.send(("good, %s" % data.decode('utf-8')).encode('utf-8'))
    sock.close()
    print('来自 %s:%s 的连接已经关闭' % addr)


# 创建永久循环来不断接受客户端请求
while True:
    # 接受一个新的连接
    sock, addr = server.accept()

    # 创建新的线程处理这个请求
    client_thread = threading.Thread(target=handle_sock, args=(sock, addr))
    client_thread.start()

写一个客户端进行测试:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8000))

# 接受欢迎信息
print(s.recv(1024).decode('utf-8'))

# 发送数据
for data in [b'mary',b'1222',b'Tom']:
    s.send(data)
    print(s.recv(1024).decode('utf-8'))

s.send(b'exit')
s.close()

首先,运行服务器端程序,然后多次运行客户端程序,结果如下:

接收到来自 127.0.0.1:55526 的请求
mary
1222
Tom
exit
来自 127.0.0.1:55526 的连接已经关闭
接收到来自 127.0.0.1:55541 的请求
mary
1222
Tom
exit
来自 127.0.0.1:55541 的连接已经关闭

可以看出,每次请求的端口号是不一样的,也就是客户端程序每次发起请求的端口号可以随机。

3.3. 问题

  • recv的参数:recv(bufsize[,flag]),bufsize指定要接收的最大数据量。应该设置多大?目前没有遇到这样的问题,但是如果设置的比较小,此时数据需要多次接收,比如下面的例子;如果设置的过大,则浪费内存。
    # 服务器端设置缓冲区大小为2的时候。
    buffer = []
    while True:
      # 获取从客户端发送的数据,一次获取1kb的数据。
      data = sock.recv(2)  # 使用sock,因为sock是建立连接之后返回的socket,用于客户端和服务器端传输数据
      print(data.decode('utf-8'))
      # 如果发送空的数据,就表示要关闭这个连接。
      if not data or data.decode('utf-8') == 'exit':
          break
      buffer.append(data)
    print((b''.join(buffer)).decode('utf-8'))
    # 客户端发送的数据
    for data in [b'mary',b'1222',b'Tom']:
      s.send(data)
    s.send(b'exit')
    此时的输出
    接收到来自 192.168.0.110:60725 的请求
    ma
    ry
    12
    22
    To
    me
    xi
    t
    mary1222Tomexit

从结果可以看出,每次接收2个字节的数据,然后放入到buffer中,最终输出全部接收的结果。

  • 客户端端口每次都不一样?当运行客户端程序的时候,通过在服务器打印客户端的端口号发现客户端的端口号每次都不一样,主要原因是客户端连接时候的端口号是操作系统分配的一个未使用的端口号。

4. UDP编程

TCP是建立可靠连接,并且通信双方是以流的形式发送数据。而UDP则是面向无连接的协议,使用UDP协议的时候,不需要建立连接,只需要知道对面的IP地址和端口号就可以直接发送数据包,但是可能存在丢包风险。

优点:比TCP速度快。

4.1. 服务器端

不需要使用listen方法,而是直接接受客户端数据 socket.SOCK_DGRAM.

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定端口:
s.bind(('127.0.0.1', 9999))

print('Bind UDP on 9999...')
while True:
    # 接收数据:
    data, addr = s.recvfrom(1024)
    print('Received from %s:%s.' % addr)
    s.sendto(b'Hello, %s!' % data, addr)

recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sendto()就可以把数据用UDP发给客户端。

4.2. 客户端

不需要使用connect()方法建立连接,直接使用sendto发送数据。

import  socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
    # 发送数据:
    s.sendto(data, ('127.0.0.1', 9999))
    # 接收数据:
    print(s.recv(1024).decode('utf-8'))
s.close()

5. WSGI-Web Server Gateway Interface-Web服务器网关接口

5.1. 介绍

WSGI是一种规范,是描述Web服务器和Web应用程序之间通信的规范,要实现WSGI协议,必须同时实现Web服务器和Web应用程序。通俗来说,只要一个服务器实现了WSGI标准模范的模块,那么任意实现了WSGI规范的应用程序都能与它进行交互。当前基于WSGI协议之上的框架有Django,Flask,Torando。

  • WSGI server负责从客户端接收请求,将request转发给application,将application返回的response返回给客户端;
  • WSGI application接收由server转发的request,处理请求,并将处理结果返回给server。application中可以包括多个栈式的中间件(middlewares),这些中间件需要同时实现server与application,因此可以在WSGI服务器与WSGI应用之间起调节作用:对服务器来说,中间件扮演应用程序,对应用程序来说,中间件扮演服务器。

5.2. 相近名词

  • uwsgi:与WSGI一样是一种通信协议,是uWSGI服务器的独占协议,用于定义传输信息的类型。
  • uWSGI:是一个Web服务器,实现了uwsgi,WSGI,http等协议

5.3. 一个HTTP请求是如何到应用程序的

可以参考这里的图here here

首先,客户端发送请求,Nginx接收到浏览器发送来的http请求.(如果是静态文件请求就直接访问用户给Nginx配置的静态文件目录,直接返回用户请求的静态文件;如果不是静态文件,而是动态请求,那么Nginx就将请求转发给实现了WSGI的服务器(uWSGI))

接着,uWSGI根据请求调用对应的应用程序来处理,然后将处理完成后的内容返回给uWSGI。

最后,uWSGI将结果返回给Nginx,Nginx将最终结果返回给浏览器

Nginx作用:参考here

  1. Nginx是一个静态服务器,能够将服务器上的静态文件通过HTTP协议展现给客户端。
  2. Nginx可以用做反向代理服务器。客户端可以直接通过HTTP协议访问应用服务器,网站管理员可以在中间增加一个Nginx服务器,此时客户端请求Nginx,Nginx请求应用服务器,然后将结果返回给客户端,此时的Nginx就是一个反向代理服务器。

反向代理服务器的好处是:可以实现负载均衡和虚拟主机。

  • 负载均衡:当网站的访问量非常大的时候,一台服务器就不够使用了。于是可以将一个应用部署在多台服务器上,将大量用户请求分配给多台服务器处理,这样的好处是,如果其中一台服务器挂掉了,其他服务器正常运行,就不影响用户使用。而Nginx就可以通过反向代理来实现负载均衡。
  • 虚拟主机:当网站访问量太小,需要节省成本的时候,可以将多台网站部署在一台服务器上。比如a.com和b.com两个网站部署在一台服务器上,两个域名的解析地址是同一个IP地址,但是用户通过两个域名却可以打开两个完全不同的网站,互相不影响,就像访问两台服务器一样,所以叫虚拟主机。

5.4. wsgiref

wsgiref是官方给出的一个实现了WSGI标准用于演示用的简单python内之苦,它实现了一个简单地WSGI 服务器和WSGI 应用程序。

wsgiref是WSGI规范的一个参考实现。

参考here
参考here
参考here