python网络编程

背景知识

  • 开发网络应用程序的核心是写出能够运行在不同的端系统和通过网络彼此通信的程序。例如,在web应用程序中,有俩个互相通信的不同的程序,一个是运行在用户主机上的浏览器程序,一个是运行在web服务器主机上的web服务器程序,当运行这俩个程序时,创建了一个客户进程和一个服务器进程,同时它们通过从套接字读出和写入数据彼此之间进行通信。开发者创建一个网络应用时,其主要任务就是编写客户程序和服务器程序的代码。
  • 在操作系统术语中,进行通信的实际上是进程,而不是程序。一个进程可以被认为是运行在端系统的一个程序。当进程运行在相同的端系统上时,它们使用进程间通信机制相互通信。进程间通信的规则由端系统上的操作系统决定。在俩个不同的端系统上的进程,通过跨越计算机网络交换报文而进行通信。发送进程生成并向网络中发送报文;接受进程接受这些报文并可能将报文发送回去进行响应。
  • 进程通过一个称为套接字(socket)的软件接口向网络发送报文和从网络接收报文。下图假定由该进程使用的下面运输层协议是因特网的TCP协议。如图,套接字是同一台主机内应用层与运输层的接口。由于该套接字是建立在网络应用程序的可编程接口,因此套接字被称为应用程序与网络之间的应用程序编程接口(API)我们可以控制套接字在应用层端的一切,对于该套接字在运输层端几乎没有控制权。对运输层的控制仅有《1》选择运输层协议《2》可以设定几个运输层参数,如最大缓存和最大报文长度,下面有例子。

python网络编程_第1张图片

  • 可供应用程序使用的运输服务要求1.可靠数据传输2.吞吐量3.定时4.安全性。 因特网为应用程序提供了俩个运输层协议,即UDP和TCP。
  1.        TCP服务提供了面向连接服务(在应用层数据报文开始流动之前,TCP让客户和服务器互相交换运输层控制信息,握手过程)和可靠的数据传送服务。
  2. UDP服务是一种不提供不必要服务的轻量级运输协议。UDP无连接,因此无握手过程。而且提供不可靠数据传送。
  • 应用层协议:刚刚学习通过把报文发送进套接字使网络进程间实现互相通信。但是如何构造这些报文?这些报文中的各个字段的含义是什么?进程何时发送这些报文?应用层协议定义了运行在不同端系统上的应用程序进程如何相互传递报文。
  • web和HTTP: web网络应用的应用层协议是超文本传输协议HTTP,它由俩个程序实现,一个客户程序一个服务器程序。客户程序与服务器程序运行在不同的端系统,通过交换HTTP报文进行交换。HTTP定义了这些报文的结构以及客户与服务器进行报文交换的方式。(web页面是由对象组成的。一个对象只是一个文件,如一个HTML文件,一个JPEG文件,一个Java小程序或一个视频片段这样的文件。)HTTP使用TCP作为他的支撑运输协议(而不是UDP)。客户向他的套接字接口发送HTTP请求报文并从它的套接字接口接收HTTP响应报文,一旦客户向他的套接字接口发送了一个请求报文,该报文脱离客户控制进入到TCP的控制,由TCP的可靠数据传送服务,该报文顺利到达服务器端的套接字接口,这里显示了分层体系结构的优点,即HTTP协议不用担心数据丢失,也不关注TCP从网络的数据丢失和乱序故障中恢复的细节,那是TCP以及协议栈较底层协议的工作。HTTP默认情况下使用持续连接,即用同一TCP连接传输一个web页面的各个对象。非持续连接每一对象使用不同的TCP传输。
  1. HTTP报文格式

                     HTTP请求报文(注意空格)

                GET  /somedir/page.html HTTP/1.1  1.1版本允许多个HTTP请求复用一个TCP连接,以加快传输速度

                Host:  www.someschool.edu 对象所在的主机(域名)服务器就需要通过Host来区分浏览器请求的是哪个网站

                Connection: close 告诉服务器不使用持续连接,服务器发送完被请求的对象后关闭这条连接

                User-agent: Mozilla/5.0

                Accept-language: fr  用户想得到该对象的法语版本(前提服务器存在这样的对象)

                请求行有三个字段:方法字段,URL字段,HTTP版本字段 GET仅请求资源,POST会附带用户数据(表单)

                      HTTP响应报文

                HTTP/1.1 200  OK

                Connection: close

                Content-Type: text/html 指示响应的内容,这里是text/html表示HTML网页。浏览器就是依靠Content-Type来判断响应的内容是网页还是图片,是视频还是音乐。浏览器并不靠URL来判断响应的内容,所以,即使URL是http://example.com/abc.jpg,它也不一定就是图片。

 

 

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

客户端

大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

举个例子,当我们在浏览器中访问新浪时,我们自己的计算机就是客户端,浏览器会主动向新浪的服务器发起连接。如果一切顺利,新浪的服务器接受了我们的连接,一个TCP连接就建立起来了,当一侧向一侧发送数据时只需经过其套接字将数据丢给TCP连接。(UDP服务器在将分组丢进套接字之前必须为其附上一个目的地地址)

所以,我们要创建一个基于TCP连接的Socket(当创建套接字时,我们将其与客户套接字地址(IP地址和端口号)和服务器套接字地址(IP地址和端口号连接起来)),可以这样做:

# 导入socket库:
import socket

# 创建一个socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('www.sina.com.cn', 80))

创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。

客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号(发送进程指定运行在接收主机上的接受进程(跟具体地说,是接受套接字))。新浪网站的IP地址可以用域名www.sina.com.cn自动转换到IP地址,但是怎么知道新浪服务器的端口号呢?

答案是作为服务器,提供什么样的服务,端口号就必须固定下来。由于我们想要访问网页,因此新浪提供网页服务的服务器必须把端口号固定在80端口,因为80端口是Web服务的标准端口。其他服务都有对应的标准端口号,例如SMTP服务是25端口,FTP服务是21端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。

因此,我们连接新浪服务器的代码如下:

s.connect(('www.sina.com.cn', 80))

注意参数是一个tuple,包含地址和端口号。

客户与服务器之间创建一条TCP连接,我们就可以向新浪服务器发送请求,要求返回首页的内容:(s.send()相当于把里面的内容放到TCP连接中)

# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。

发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:

# 接收数据:
buffer = []
while True:
    # 每次最多接收1k字节:
    d = s.recv(1024)
    if d:
        buffer.append(d)
    else:
        break
data = b''.join(buffer)

接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。

当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:

# 关闭连接:
s.close()

接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:

header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open('sina.html', 'wb') as f:
    f.write(html)

现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。

服务器

和客户端编程相比,服务器编程就要复杂一些。

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

所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。

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

我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello再发回去。

首先,创建一个基于IPv4和TCP协议的Socket:

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

然后,我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。

端口号需要预先指定。因为我们写的这个服务不是标准服务,所以用9999这个端口号。请注意,小于1024的端口号必须要有管理员权限才能绑定:

# 监听端口:
s.bind(('127.0.0.1', 9999))

紧接着,调用listen()方法开始监听端口,即聆听来自客户的TCP连接请求,传入的参数指定等待连接的最大数量:

s.listen(5)
print('Waiting for connection...')

接下来,服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:程序s调用accept(),这在服务器中创建了一个称为sock的新套接字(以前服务器的套接字s被称为欢迎套接字),由这个特定的用户使用,addr存放客户的IP地址和端口号。(这样你就有两个套接字了,原来的一个还在侦听你的那个端口,   新的sock在准备发送   send() 和接收      recv() 数据。

while True:
    # 接受一个新连接:
    sock, addr = s.accept()
    # 创建新线程来处理TCP连接:
    t = threading.Thread(target=tcplink, args=(sock, addr))
    t.start()

每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接:

def tcplink(sock, addr):
    print('Accept new connection from %s:%s...' % addr)
    sock.send(b'Welcome!')
    while True:
        data = sock.recv(1024)
        time.sleep(1)
        if not data or data.decode('utf-8') == 'exit':
            break
        sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
    sock.close()
    print('Connection from %s:%s closed.' % addr)

连接建立后,服务器首先发一条欢迎消息,第一个print打印出客户的IP地址和端口号。然后等待客户端数据,并加上Hello再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。

要测试这个服务器程序,我们还需要编写一个客户端程序(发送变成大写后的数据,需先解码,再按utf-8编码, encode()其实就是encode('utf-8')):

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
    # 发送数据:
    s.send(data)
    print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()

我们需要打开两个命令行窗口,一个运行服务器程序,另一个运行客户端程序,就可以看到效果了:

┌────────────────────────────────────────────────────────┐
│Command Prompt                                    - □ x │
├────────────────────────────────────────────────────────┤
│$ python echo_server.py                                 │
│Waiting for connection...                               │
│Accept new connection from 127.0.0.1:64398...           │
│Connection from 127.0.0.1:64398 closed.                 │
│                                                        │
│       ┌────────────────────────────────────────────────┴───────┐
│       │Command Prompt                                    - □ x │
│       ├────────────────────────────────────────────────────────┤
│       │$ python echo_client.py                                 │
│       │Welcome!                                                │
│       │Hello, Michael!                                         │
└───────┤Hello, Tracy!                                           │
        │Hello, Sarah!                                           │
        │$                                                       │
        │                                                        │
        │                                                        │
        └────────────────────────────────────────────────────────┘

需要注意的是,客户端程序运行完毕就退出了,而服务器程序会永远运行下去,必须按Ctrl+C退出程序。

你可能感兴趣的:(python网络编程)