第十五章 TCP与UDP编程

 

15.1 套接字

套接字(Socket)是用于网络通信的数据结构。在任何类型的通信开始之前,都必须创建Socket。可以将它们比作电话插孔,没有它就无法进行通信。

Socket主要分为面向连接的Socket 和无连接Socket。面向连接的Socket使用的主要协议是传输控制协议,也就是常说的TCP,TCP的Socket名称是SOCK_STREAM。无连接Socket的主要协议是用户数据报协议,也就是常说的 UDP,UDP Socket的名字是SOCK_DGRAM

15.1.1 建立 TCP 服务端

Socket分为客户端和服务端。客户端Socket用于建立与服务端Socket 的连接,服务端Socket用于等待客户端Socket 的连接。因此,在使用客户端Socket建立连接之前,必须建立服务端Socket。

服务端Socket 除了要指定网络类型(IPv4和IPv6)和通信协议(TCP 和 UDP)外,还必须要指定一个端口号。所有建立在TCP/UDP之上的通信协议都有默认的端口号。例如,HTTP协议的默认端口号是80,HTTPS协议的默认端口号是443,FTP协议的默认端口号是21。

在Python语言中创建Socket服务端程序,需要使用socket模块中的socket类。创建Socket服务端的步骤如下:

(1)创建Socket对象。
(2)绑定端口号。
(3)监听端口号。
(4)等待客户端Socket的连接。
(5) 读取从客户端发送过来的数据。
(6)向客户端发送数据。
(7)关闭客户端Socket 连接。
(8) 关闭服务端 Socket连接。

# 创建socket对象
tcpServerSocket = socket(...)
# 绑定Socket服务端端口号
tcpServerSocket.bind(...)
# 监听端口号
tcpServerSocket.listen(...)
# 等待客户端的连接(可循环执行多次,串行的处理客户端的请求)
tcpClientSocket = tcpServerSocket.accept()
# 读取服务端发送过来的数据
data= tcpClientSocket.recv(...)
# 向客户端发送数据
tcpClientSocket.send(...)
# 关闭客户端Socket连接
tcpClientSocket.close()
# 关闭服务端Socket连接
tcpServerSocket.close()

示例:使用socket模块中的相关API建立一个Socket服务端,端口号是9876,可以使用浏览器、telnet等客户端软件测试这个Socket服务。

from socket import *
# 定义一个空的主机名,在建立服务端Socket时一般不需要使用host
host = ''
# 用于接收客户端数据时的缓冲区尺寸,也就是每次接收的最大数据量(单位:字节)
bufferSize = 1024
# 服务端Socket的端口号
port = 9876
# 将host和port封装成一个元组
addr = (host,port)
# 创建Socket对象,AF_INET表示IPV4,AF_INET6表示IPV6,SOCK_STREAM表示TCP
tcpServerSocket = socket(AF_INET, SOCK_STREAM)
# 使用bind绑定端口号
tcpServerSocket.bind(addr)
# 监听端口号
tcpServerSocket.listen()

print('Server port:9876')
print('正在等待客户端连接')
# 等待客户端Socket的连接,这里的程序会被阻塞,直到接收到客户端的连接请求,才会往下执行
# 接收到客户端请求后,同时返回了客户端Socket和客户端的端口号
tcpClientSocket,addr = tcpServerSocket.accept()
print('客户端已经连接','addr','=',addr)
# 开始读取客户端发送过来的数据,每次最多会接收不超过bufferSize字节的数据
# 如果客户端发送的数据超过bufferSize字节,剩下的会等待recv方法的下一次读取
data = tcpClientSocket.recv(bufferSize)
# recv方法返回了字节形式的数据,如果要使用字符串,需要将其进行解码,本例使用了utf8格式解码
print(data.decode('utf8'))
# 向客户端以utf-8格式发送数据
tcpClientSocket.send('你好,I love you.\n'.encode(encoding='utf_8'))
# 关闭客户端Socket
tcpClientSocket.close()
# 关闭服务端Socket
tcpServerSocket.close()

telnet测试
在这里插入图片描述
在这里插入图片描述
浏览器测试: http://localhost:9876/ufo

在这里插入图片描述
在这里插入图片描述
浏览器报错(因为是http协议),后台显示连接成功,说明客户端到夫妇段测试是通畅的。

15.1.2 服务端接收数据的缓冲区

如果客户端传给服务端的数据过多,则需要分多次读取,每次最多读取缓冲区尺寸的数据,也就是bufferSize变量的值。如果要分多次读取,则根据当前读取的字节数是否小于缓冲区的尺寸来判断是否后面还有其他未读的数据,如果没有,则终止循环。

from socket import *

host = ''
# 将缓冲区设为2
bufferSize = 2
port = 9876
addr = (host,port)
tcpServerSocket = socket(AF_INET, SOCK_STREAM)
tcpServerSocket.bind(addr)
tcpServerSocket.listen()
print('Server port:9876')
print('正在等待客户端连接')
tcpClientSocket,addr = tcpServerSocket.accept()
print('客户端已经连接','addr','=',addr)
# 初始化一个bytes类型的变量,用于保存完整的客户端数据
fullDataBytes = b''
while True:    
    # 每次最多取2个字节的数据
    data = tcpClientSocket.recv(bufferSize)
    # 将读取的字节数添加到fullDataBytes变量的后边
    fullDataBytes += data
    # 如果读取的字节数小于fullDataBytes,则终止循环
    if len(data) < bufferSize:
        break;
# 按原始字节格式输出客户端发送过来的信息
print(fullDataBytes)
# 将原始字节格式数据用ISO-8859-1格式解码,然后输出
# 客户端如果用其他UTF-8解码等,恰巧无法解码,则会抛出报错
print(fullDataBytes.decode('ISO-8859-1'))
tcpClientSocket.close()
tcpServerSocket.close()

15.1.3 服务端的请求队列

通常服务端程序不会只为一个客户端服务,当accept方法在接收到一个客户端请求后,除非再次调用accept方法,否则将不会再等待下一个客户端请求。当然,可以在accept方法接收到一个客户端请求后启动一个线程来处理当前客户端的请求,从而让 accept方法尽可能快地再次被调用(一般会将调用accept方法的代码放在一个循环中),但就算 accept方法很快被下一次调用,也是有时间间隔的限制 (如两次调用accept方法的时间间隔是100ms),如果在这期间又有客户端请求,该如何处理呢?

在服务端Socket中有一个请求队列。如果服务端暂时无法处理客户端请求,会先将客户端请求放到这个队列中,而每次调用accept方法,都会从这个队列中取一个客户端请求进行处理。不过这个请求队列也不能无限制地存储客户端请求,请求队列的存储上限与当前操作系统有关,例如,有的Linux系统的请求队列存储上限是128个。请求队列的存储上限也可以进行设置,通过listen方法监听端口号时指定请求队列上限即可(这个值也被称为 backlog),如果这个指定的上限超过了操作系统限制的最大值(如128),那么会直接使用这个最大值。

from socket import *

host = ''
bufferSize = 1024
port = 9876
addr = (host,port)
tcpServerSocket = socket(AF_INET, SOCK_STREAM)
tcpServerSocket.bind(addr)
# 设置服务端Socket请求队列的backlog值为2
tcpServerSocket.listen(2)
print('Server port:9876')
print('正在等待客户端连接')
# 可多次调用,但是有backlog限制
while True:
    tcpClientSocket,addr = tcpServerSocket.accept()
    print('客户端已经连接','addr','=',addr)
    data = tcpClientSocket.recv(bufferSize)
    print(data.decode('utf8'))
    tcpClientSocket.send('你好,I love you.\n'.encode(encoding='utf_8'))
    tcpClientSocket.close()

tcpServerSocket.close()

客户端可以启动4个连接,由于存在请求队列,因此前边3个都可以正常连接服务端(一个正常连接,两个占用请求队列),所以第四个客户端请求会一直尝试连接,直到连接超时而终止连接

15.1.4 TCP 时间戳服务端

当客户端向服务端发送数据后,服务端会将这些数据原样返回,并附带上服务端当前的时间。

from socket import *
from time import ctime

# 时间戳服务器
host = ''
bufferSize = 1024
port = 9876
addr = (host,port)
tcpServerSocket = socket(AF_INET, SOCK_STREAM)
tcpServerSocket.bind(addr)
tcpServerSocket.listen(5)
while True:
    print('正在等待客户端连接')
    tcpClientSocket,addr = tcpServerSocket.accept()
    print('客户端已经连接','addr','=',addr)
    while True:
        # 接收客户端发送过来的数据
        data = tcpClientSocket.recv(bufferSize)
        if not data:
            break;
        # 获取当前时间,返回客户端原始数据+时间信息
        tcpClientSocket.send(ctime().encode(encoding='utf-8') + b' ' + data)
    tcpClientSocket.close()
tcpServerSocket.close()

15.1.5 用 Socket 实现 HTTP 服务端

首先简单了解一下HTTP请求头的格式。

GET /main/index.html HTTP/1.1
Host: geekori.com
Accept: */*
Pragma: no-cache
Cache-Control: no-cache
Referer: http://download.microtool.de/
User-Agent: Mozilla/4.04[en](win95;I;Nav)
Range: bytes=554554-

浏览器会自动发送这些内容,服务端也会自动处理。但必须了解第1行,因为在第1行中包含了请求路径。第1行分为如下三个部分,中间用空格分隔。

  • 方法(GET、POST 等)。
  • 请求路径,需要将其映射成服务端对应的本地文件路径。
  • HTTP版本,目前一般是1.1。

HTTP响应头与HTTP请求头类似,下面是一个 HTTP响应头的例子。

HTTP/1.1 200 OK
Date: Mon,31Dec201204:25:57GMT
server: Apache/1.5 (UNIX)
content-type: text/ htmlContent-length:1234

HTTP响应头只有两行是必须指定的,即第1行和 Content-length字段。第1行描述了HTTP版本、返回状态码等信息,其中 200和OK描述了访问成功。如果要描述页面没找到,可以返回404。例子中不管是否找到服务端的页面,都返回了200,只是在没找到页面时固定返回了“File NotFound”信息。

注意,HTTP响应头在返回时,一定与后面的要返回的内容之间有一个空行,浏览器会依赖这个空行区分HTTP响应头到哪里结束。

from socket import *
import os

# 用于从文本读取要返回的HTTP响应头的文本,并设置返回数据长度legent 
def responseHeaders(file,length):
    f = open(file,'r')
    
    headersText = f.read()
    headersText = headersText % length
    return headersText
# 根据HTTP请求头的路径的得到服务器的本地路径
def filePath(get):
    if get == '/':
        # 如果访问的是根目录,那么默认访问/static/index.html
        return 'static' + os.sep + 'index.html'
    else:
        paths = get.split('/')
        s = 'static'
        # HTTP请求头中的路径与服务器的本地路径是一致的,只是把相应的分隔符换成对应的操作系统的
        for path in paths:
            if path.strip() != '':
                s = s + os.sep + path 
        return s

host = ''
bufferSize = 1024
port = 9876
addr = (host,port)
tcpServerSocket = socket(AF_INET, SOCK_STREAM)
tcpServerSocket.bind(addr)
tcpServerSocket.listen(5)
while True:
    print('正在等待客户端连接')
    tcpClientSocket,addr = tcpServerSocket.accept()
    print('客户端已经连接','addr','=','addr')
    data = tcpClientSocket.recv(bufferSize)
    data = data.decode('utf-8')
    try:
        # 获取HTTP请求头的第1行字符串,这一行也包含了请求路径
        firstLine = data.split('\n')[0]
        # 获取请求路径
        path = firstLine.split(' ')[1]
        print(path)
        # 将HTTP请求路径转换为服务器的本地路径
        path = filePath(path)
        #如果文件存在,读取文件的全部内容
        if os.path.exists(path):
            file = open(path,'rb')
            content = file.read()
            file.close()        
        else:
            # 如果文件不存在,向客户端发送 "File Not Found"
            content = '<h1>File Not Found</h1>'.encode(encoding='utf-8')
        # 从文件读取生成的HTTP响应头信息,并返回数据的长度(单位:字节)
        rh = responseHeaders('response_headers.txt',len(content)) + '\r\n' 
        # 连同HTTP响应头与返回数据一同发送给客户端
        tcpClientSocket.send(rh.encode(encoding='utf-8') + content)
            
    except Exception as e:
        print(e)
    tcpClientSocket.close()
tcpServerSocket.close()

浏览器测试

在当前目录下创建一个static子目录,并在该目录下创建两个文件:test.txt 和 index.html

# test.txt
hello world

# index.html
<h1>Main Page</h1>

接下来在当前目录建立一个response_headers.txt文件,并输入如下内容。在该文件中使用了“%d”作为格式化符号,在读取该文件时需要将“%d”格式化为发送到客户端数据的长度,单位是字节。

HTTP/1.1 OK
Server: custom
content-type: text/html
Content-length: %d

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

15.1.6 客户端 Socket

在前面的部分一直使用teInet和浏览器作为客户端测试Socket服务端,其实socket类同样可以作为客户端连接服务器。socket类连接服务端的方式与创建Socket服务端类似,只是这时host (IP或域名)就有用了,因为客户端Socket 在连接服务端时,必须指定服务器的IP或命名。当然,端口号也是必需的。在浏览器中使用http/https访问Web页面时,之所以没有指定端口号,是因为使用了默认的端口号,http 的默认端口号是80,https 默认的端口号是443。

客户端Socket 成功连接服务端后,可以使用send方法向服务端发送数据,也可以使用recv方法接收从服务端返回的数据,使用方法与服务端Socket相同。

from socket import *

# 服务器名称可以是IP或域名
host = 'localhost'
port = 9876
bufferSize = 1024
addr = (host,port)
# 开始连接时间戳服务器
tcpClientSocket = socket(AF_INET, SOCK_STREAM)
tcpClientSocket.connect(addr)
while True:
    # 从终端采集用户输入信息
    data = input('>')
    # 什么都没有输入则推出
    if not data:
        break
    # 将用户输入按照utf-8编码成字节序列 
    data = data.encode('utf-8')
    # 向服务器发送字节形式的数据
    tcpClientSocket.send(data)
    # 从服务器接收数据
    data = tcpClientSocket.recv(bufferSize)
    # 输出从服务器接收到的数据
    print(data.decode('utf-8'))
#关闭客户端Socket
tcpClientSocket.close()

在这里插入图片描述

在这里插入图片描述

15.1.7 UDP 时间戳服务端

UDP与TCP的一个显著差异就是前者不是面向连接的,也就是说,UDP Socket是无连接的,TCP Socket是有连接的。那么什么是无连接?什么是有连接呢?有连接的网络传输协议(如TCP)是指在网络数据传输的过程中客户端与服务端的网络连接会一直存在,而且面向连接的网络传输协议会通过某些机制保证数据传输的可达性,如果用比较科幻的说法就是通过面向连接的网络协议在客户端和服务端建立一个稳定的虫洞,可以放心大胆地在虫洞中传递数据。面向无连接的网络协议(如UDP)相当于将一束光射向远方,对于发射光源的一方只负责开启光源,至于射出的这束光能不能到达目的地,那就不管了。当然,这束光有可能会到达目的地,也有可能发生意外,如碰到某个障碍物或被散射因此,通过像UDP这类无连接的网络协议传输的数据不能保证100%到达目的地,但操作更简单,没有像TCP这类有连接的网络协议需要那么多设置。

实现一个时间戳服务器:

from socket import *
from time import ctime
host = ''
port = 9876
bufferSize = 1024
addr = (host, port)
# SOCK_DGRAM表示UDP
udpServerSocket = socket(AF_INET, SOCK_DGRAM)
udpServerSocket.bind(addr)
while True:
    print('正在等待消息......')
    # 接收从客户端发送过来的数据
    data, addr = udpServerSocket.recvfrom(bufferSize)
    # 向客户端发送 时间信息+接收到的字符串
    udpServerSocket.sendto(ctime().encode(encoding='utf-8') + b' ' + data,addr)
    print('客户端地址:',addr)
udpServerSocket.close()

注意,使用UDP Socket 发送和接收数据的方法与TCP Socket不同。UDP Socket 接收数据的方法是recvfrom,发送数据的方法是sendto。

15.1.8 UDP 时间戳客户端

from socket import *
host = 'localhost'
port = 9876
bufferSize = 1024
addr = (host, port)
udpClientSocket = socket(AF_INET, SOCK_DGRAM)
while True:
    # 从终端采集数据
    data = input('>')
    if not data:
        break
    # 向服务器发送数据
    udpClientSocket.sendto(data.encode(encoding='utf-8'),addr)
    # 接收服务器返回来的数据
    data,addr = udpClientSocket.recvfrom(bufferSize)
    if not data:
        break
    print(data.decode('utf-8'))
udpClientSocket.close()

可以向UDP时间戳服务端发送数据进行测试。
在这里插入图片描述
在这里插入图片描述

15.2 socketserver 模块

socketserver 是标准库中的一个高级模块,该模块的目的是让Socket编程更简单。在 socketserver模块中提供了很多样板代码,这些样板代码是创建网络客户端和服务端所必需的代码。利用socketserver模块中的API也可以实现时间戳客户端和服务端且代码更简洁,也更容易维护。

15.2.1 实现 socketserver TCP 时间戳服务端

socketserver模块中提供了一个 TCPServer类,用于实现TCP服务端。TCPServer类的构造方法有两个参数 : 第1个参数需要传入host和 port(元组形式);第2个参数需要传入一个回调类,该类必须是StreamRequestHandler类的子类。在StreamRequestHandler类中需要实现一个handle方法,如果接收到客户端的响应,那么系统就会调用handle方法进行处理,通过 handle方法的self参数中的响应API可以与客户端进行交互。

# 将TCPServer类重命名为TCP,将StreamRequestHandler类重命名为SRH
from socketserver import (TCPServer as TCP,StreamRequestHandler as SRH)
from time import ctime

host = ''
port = 9876
addr = (host,port)
# 定义回调函数,该类必须从SRH类继承
class MyRequestHandler(SRH):
    # 处理客户端请求得方法
    def handle(self):
        # 获取并输出客户端IP和端口号
        print('客户端已经连接,地址:',self.client_address)
        # 向客户端发送服务端的 时间+接收到的字符串
        self.wfile.write(ctime().encode(encoding='utf-8') + b' ' + self.rfile.readline())
print('正在等待客户端的连接')
# 调用serve_forever方法让服务器端等待客户端的连接
tcpServer = TCP(addr, MyRequestHandler)
tcpServer.serve_forever()

从上面的代码可以看出,在 handle方法中通过 self.rfile.readline方法从客户端读取数据,通过self.wfile.write方法向客户端发送数据。由于读取客户端数据使用了readline方法,该方法读取客户端发送过来的数据的第1行,所以客户端发送过来的数据至少要有一个行结束符(“\r\n”或“\n"),否则服务端在读取客户端发送过来的数据时会一直处于阻塞状态,直到超时才结束读取。

15.2.2 实现 socketserver TCP 时间戳客户端

from socket import *
host = 'localhost'
port = 9876
bufferSize = 1024
addr = (host, port)
while True:
    tcpClientSocket = socket(AF_INET, SOCK_STREAM)
    tcpClientSocket.connect(addr)
    # 从终端采集要发送的数据
    data = input('>')
    if not data:
        break
    # 向时间服务端发送数据,并添加行结束符
    tcpClientSocket.send(('%s\r\n' % data).encode(encoding='utf-8'))
    # 接收时间服务端返回来的数据
    data = tcpClientSocket.recv(bufferSize)
    if not data:
        break
    # 输出接收到的数据
    print(data.decode('utf-8').strip())
    # 关闭客户端Socket
    tcpClientSocket.close()

时间戳客户端与服务端互通测试。
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值