TCP与Web服务器

TCP与Web服务器

  TCP服务是面向连接的服务,其传输服务流程:客户机向服务器发送连接请求,服务器确定连接请求并与客户机程序交换控制信息,在两个进程的套接字之间建立一个TCP连接;客户机向服务器发送请求报文,服务器接收请求报文,产生响应报文并发送到套接字,服务器关闭TCP连接;客户机接受响应报文。
  Web的应用层协议是超文本传输协议(HTTP)。HTTP由两个程序实现,客户机程序与服务器程序。
  HTTP请求报文格式:
在这里插入图片描述
  HTTP响应报文格式:

在这里插入图片描述

Python实现套接字编程

实验准备

(1)自定义编写的一个myfile.html文件,表示客户机向服务器申请的文件。为了方便访问,该文件存放在与服务器程序同一目录下。
在这里插入图片描述

(2)自定义编写的一个error.html文件,如果服务器未能找到客户机申请访问的文件,则将该文件返回给客户机(实际上,该文件就是404 Not Found)。同样为了方便访问,该文件存放在与服务器程序同一目录下。
在这里插入图片描述

服务器端程序

# TCP服务器程序:TCPServer.py
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
import numpy as np
from socket import *
serverSocket = socket(AF_INET,SOCK_STREAM)    # 生成服务器的TCP连接套接字
serverPort = 6121     # 端口号
serverSocket.bind(("",serverPort))  # 绑定服务器套接字和端口号
serverSocket.listen(10)
print("服务器已启动,正在提供服务......")
while True:
    connectionSocket,address = serverSocket.accept()   # 根据客户创建一个TCP连接
    try :
        # 接收请求报文并读取文件
        message = connectionSocket.recv(1024).decode()  # 接收客户机的请求报文
        print("接收到请求报文的时间:",time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())),np.mod(time.time(),60),sep="")
        print("已接收到请求报文:\n", message)
        filename = message.split()[1]        # 解析请求报文,获取文件名
        with open(filename[1:], "r") as f:
            content = f.read()     # 根据文件名读取文件内容
        ## 生成响应报文(状态行+首部行+文件内容)
        stateRow = "HTTP/1.1 200 OK\r\n"     # 状态行
        firstRow = "Connection close\r\nDate:"+time.strftime("%Y-%m-%d",time.localtime(time.time()))+"\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:"+str(len(content))+"\r\nContent-Type:html\r\n\r\n"  # 首部行
        outputdata = stateRow+firstRow+content  # 响应报文=状态行+首部行+文件内容
        connectionSocket.send(outputdata.encode())   # 返回响应报文字节流
        connectionSocket.close()   # 关闭TCP连接
    except IOError:   # 抛出异常
        print("[ERROR]The file being fetched is not existed.")
        with open("error.html","r") as f:   # 异常则打开异常响应文件(404)
            content = f.read()
        ## 生成响应报文(状态行+首部行+文件内容)
        stateRow = "HTTP/1.1 404 Not Found\r\n"  # 状态行
        firstRow = "Connection close\r\nDate:" + time.strftime("%Y-%m-%d", time.localtime(
            time.time())) + "\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:" + str(
            len(content)) + "\r\nContent-Type:html\r\n\r\n"  # 首部行
        outputdata = stateRow + firstRow + content  # 响应报文=状态行+首部行+文件内容
        connectionSocket.send(outputdata.encode())  # 返回错误响应字节流
        connectionSocket.close()   # 关闭TCP连接

  服务器端程序代码解析:生成服务器套接字 serverSocket,绑定服务器主机与指定的端口号(此处端口号选用6121),服务器保持等待,等待客户机请求。有客户机请求连接时,根据客户机创建一个TCP连接connectionSocketaddress,在服务器端查找请求报文中请求的文件,如果能够找到该文件,那么将该文件以HTTP响应报文的格式封装后(读取文件内容content,加上响应报文的头部信息)发送给客户机,否则发送error.html文件。随后关闭TCP连接,继续等待下一个用户。

客户机端程序

# TCP客户机程序:TCPClient.py
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
from socket import *
import numpy as np
serverName = "192.168.0.104"  # 服务器主机
serverPort = 6121         # 端口号
clientsocket = socket(AF_INET,SOCK_STREAM)      # 创建客户机套接字
clientsocket.connect((serverName,serverPort))   # 建立连接
## 发送请求报文并接收服务器的回复
fetch_file = "/myfile.html"  # 需要请求的文件
requestRow = "Get "+fetch_file+" HTTP/1.1\r\n"   # 请求行
firstRow = "Host:192.168.0.104\r\nUser-agent:Microsoft Edge/100.0.1185.36\r\nConnection:close\r\nAccept-language:ch\r\n\r\n"  # 首部行
requestMessages = requestRow+firstRow   # 请求报文(请求行+首部行)
print("请求报文发出时间:",time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())),np.mod(time.time(),60),sep="")

start = time.perf_counter()
clientsocket.send(requestMessages.encode())   # 发送请求报文
responseMessage = clientsocket.recv(1024)     # 接收服务器的回复
end = time.perf_counter()
print("RTT:",end-start,"s")

print("响应报文:\n",responseMessage.decode(),sep = "")
## 生成本地html文件
f = open('localHtml.html','w')
message = responseMessage.decode()   # 报文解码
message = message.split("\r\n")      # 解析报文
content = message[-1]                # 获取向服务器申请的文件
f.write(content)      # 保存到本地文件中
f.close()
## 关闭套接字
clientsocket.close()

  客户机端程序代码解析:生成客户机套接字 clientSocket,绑定服务器主机与指定的端口号(此处端口号选用6121)并向服务器发送连接请求,服务器确认连接后,将需要访问的文件信息按照请求报文的格式封装后发送给服务器,等待服务器的回传。接收服务器的响应报文,解析报文,提取出文件数据,保存到本地 localHtml.html,关闭连接。

运行程序步骤

(1)运行服务器程序TCPServer.py,保持服务器在正常工作中,即不能终止程序。
(2)运行客户机程序TCPClient.py

测试运行结果

(1)利用客户机TCPClient.py程序测试服务器:
 (a)调用客户机程序,访问服务器中存在的文件(即TCPClient.pyfetch_file = “/myfile.html”

服务器端控制台
客户机端控制台
localHtml.html文件

 (b)调用客户机程序,访问服务器中不存在的文件(可设置TCPClient.pyfetch_file = “/file.html”

服务器端控制台
客户机端控制台
localHtml.html文件

(2)利用浏览器直接请求服务器(服务器IP和端口号:192.168.0.104:6121)
  (a)请求访问存在的文件:
在这里插入图片描述
 (b)请求访问不存在的文件:
在这里插入图片描述

服务器端优化

缺陷

  当有多个客户机同时向服务器TCPServer.py发送TCP连接请求时,服务器必须与前一个客户机断开TCP连接后才能与下一个客户机建立连接,这影响了服务器的性能。

实验准备

  服务器TCPServer.py程序决定了它与客户机的TCP连接的时间几乎是很短的。因此,为了模拟下一个客户机请求到达时上一个客户机仍与服务器保持TCP连接,做以下的操作:
  (1)为了模拟客户机长时占用服务器,即TCP连接时间较长。修改服务器程序,在服务器向客户机发送响应报文后,让服务器延迟断开TCP连接。代码修改部分如下:在这里插入图片描述
  (2)为了让多个客户机访问服务器,只需要多次运行客户机程序即可。(不能确保这十次请求是同时发生的,但是发送10次请求报文的时间在3秒内完成,又由于服务器返回报文需要等待10秒,所以可以模拟认为10次请求是同时发生的)
 (注:pycharm可在‘运行/调试配置’中打开‘允许并行允许’,可以让客户机程序同时运行多次)

服务器优化前程序运行结果

  运行增加延迟断开TCP连接的服务器程序TCPServer.py,连续运行客户机程序TCPClient.py十次。
  记录客户机发送请求报文和服务器接收到报文的时刻,计算客户机发送请求报文到接收响应报文的时间差,作为一个时延RRT。实验得到的数据如下表

序号客户机请求报文的发送时间
(%Y-%m-%d %H:%M:%S)
服务器接收到请求报文的时间
(%Y-%m-%d %H:%M:%S)
时延RRT
(秒/s)
12022-04-14 14:36:47.1382022-04-14 14:36:47.96410.0027
22022-04-14 14:36:47.4472022-04-14 14:36:57.97419.7024
32022-04-14 14:36:47.7092022-04-14 14:37:07.98129.4850
42022-04-14 14:36:48.0192022-04-14 14:37:17.99339.1469
52022-04-14 14:36:48.3562022-04-14 14:37:28.00048.8227
62022-04-14 14:36:48.6692022-04-14 14:37:38.00858.5192
72022-04-14 14:36:48.9402022-04-14 14:37:48.01768.2455
82022-04-14 14:36:49.2232022-04-14 14:37:58.01877.9665
92022-04-14 14:36:49.4822022-04-14 14:38:08.02387.7094
102022-04-14 14:36:49.8022022-04-14 14:38:18.02597.3891

  分析表可知,在服务器与一个客户机建立连接时,当有其他客户机请求连接时,必须要等到前一个客户与服务器断开连接。因此在时延上就表现为:即使是10个客户机是“同时”向服务器发送请求的,相对晚连接的客户机的时延相对较大,最后一个申请连接的客户经过长时间的等待,造成巨大的时延。

服务器程序优化

  多线程原理:进程是系统正在运行的应用程序,线程是进程的基本就行单元,进程的所有任务都在线程中执行。一个线程中任务的执行是串行的,当一个线程中执行多个任务时只能按照顺序执行这些任务,即在同一时间内只能执行一个任务。一个进程可以开启多条线程,每条线程可以同时并发执行不同的任务,能够提高程序的执行效率和资源利用率。多线程并发执行,本质上是CPU快速在多条线程之间切换。
  Python可以利用threading模块中的Thread类的构造器创建线程。即直接对类threading.Thread进行实例化创建线程,并且通过调用start()方法启动线程。

  将初始服务器程序TCPServer.py中接收请求报文部分代码包装为函数TCPServer(),放入threading.Thread()中进行优化。得到的多进程Web服务器程序Mul_TCPServer.py如下:

# 多进程TCP服务器程序:Mul_TCPServer.py
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
import numpy as np
from socket import *
import threading
def TCPServer(connectionSocket,address):
    # 将实验1中的服务器程序包装成函数
    try:
        # 接收请求报文并读取文件
        message = connectionSocket.recv(1024).decode()  # 接收客户机的请求报文
        print("接收到请求报文的时间:", time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())), np.mod(time.time(), 60), sep="")
        print("已接收到请求报文:\n", message)
        filename = message.split()[1]  # 解析请求报文,获取文件名
        with open(filename[1:], "r") as f:
            content = f.read()  # 根据文件名读取文件内容
        ## 生成响应报文(状态行+首部行+文件内容)
        stateRow = "HTTP/1.1 200 OK\r\n"  # 状态行
        firstRow = "Connection close\r\nDate:" + time.strftime("%Y-%m-%d", time.localtime(
            time.time())) + "\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:" + str(
            len(content)) + "\r\nContent-Type:html\r\n\r\n"  # 首部行
        outputdata = stateRow + firstRow + content  # 响应报文 = 状态行+首部行+文件内容
        time.sleep(10)    # 使服务器睡眠10秒,模拟堵塞,便于实现多次请求
        connectionSocket.send(outputdata.encode())  # 返回响应报文字节流
        connectionSocket.close()  # 关闭TCP连接
    except IOError:  # 抛出异常
        print("[ERROR]The file being fetched is not existed.")
        with open("error.html", "r") as f:    # 出现异常则返回错误的网页(404)
            content = f.read()
        ## ## 生成响应报文(状态行+首部行+文件内容)
        stateRow = "HTTP/1.1 404 Not Found\r\n"  # 状态行
        firstRow = "Connection close\r\nDate:" + time.strftime("%Y-%m-%d", time.localtime(
            time.time())) + "\r\n服务器:Apache/1.3.0 (Windows)\r\nLast-Modified:Wedn,13 April 2022\r\nContent-Length:" + str(
            len(content)) + "\r\nContent-Type:html\r\n\r\n"  # 首部行
        outputdata = stateRow + firstRow + content  # 响应报文 = 状态行+首部行+文件内容
        time.sleep(10)   # 使服务器睡眠10秒,模拟堵塞,便于实现多次请求
        connectionSocket.send(outputdata.encode())  # 返回错误响应字节流
        connectionSocket.close()   # 关闭TCP连接


serverSocket = socket(AF_INET, SOCK_STREAM)  # 生成服务器的TCP连接套接字
serverPort = 6121  # 端口号
serverSocket.bind(("", serverPort))  # 绑定服务器套接字和端口号
serverSocket.listen(10)    # 聆听客户连接
while True:
    connectionSocket,address = serverSocket.accept()    # 等待连接
    thread = threading.Thread(target=TCPServer, args=(connectionSocket,address))    # 加入线程,多线程进行处理
    thread.start()

  对应的,客户机程序Mul_TCPClient.py也做出部分修改(实现任务与初始客户机TCPClient.py一致,只是修改时间表示部分):

# TCP客户机程序
# 服务器IP:192.168.0.104
# 客户机IP:192.168.0.102
import time
from socket import *
import numpy as np
serverName = "192.168.0.104"  # 服务器主机
serverPort = 6121         # 端口号
clientsocket = socket(AF_INET,SOCK_STREAM)      # 创建客户机套接字
clientsocket.connect((serverName,serverPort))   # 建立连接
## 发送请求报文并接收服务器的回复
fetch_file = "/myfile.html"  # 需要请求的文件
requestRow = "Get "+fetch_file+" HTTP/1.1\r\n"   # 请求行
firstRow = "Host:192.168.0.104\r\nUser-agent:Microsoft Edge/100.0.1185.36\r\nConnection:close\r\nAccept-language:ch\r\n\r\n"  # 首部行
requestMessages = requestRow+firstRow   # 请求报文(请求行+首部行)
print("请求报文发出时间:",time.strftime("%Y-%m-%d %H:%M:", time.localtime(time.time())),np.mod(time.time(),60),sep="")

start = time.perf_counter()
clientsocket.send(requestMessages.encode())   # 发送请求报文
responseMessage = clientsocket.recv(1024)     # 接收服务器的回复
end = time.perf_counter()
print("RTT:",end-start,"s")

print("响应报文:\n",responseMessage.decode(),sep = "")
## 生成本地html文件
f = open('localHtml.html','w')
message = responseMessage.decode()   # 报文解码
message = message.split("\r\n")      # 解析报文
content = message[-1]                # 获取向服务器申请的文件
f.write(content)      # 保存到本地文件中
f.close()
## 关闭套接字
clientsocket.close()

服务器优化后程序运行结果

  运行多进程服务器程序Mul_TCPServer.py,连续运行客户机程序Mul_TCPClient.py十次。
  记录客户机发送请求报文和服务器接收到报文的时刻,计算客户机发送请求报文到接收响应报文的时间差,作为一个时延RRT。实验得到的数据如下表

序号客户机请求报文的发送时间
(%Y-%m-%d %H:%M:%S)
服务器接收到请求报文的时间
(%Y-%m-%d %H:%M:%S)
时延RRT
(秒/s)
12022-04-14 15:15:43.9772022-04-14 15:15:44.83610.0292
22022-04-14 15:15:44.2212022-04-14 14:15:45.07710.0191
32022-04-14 15:15:44.4232022-04-14 14:15:45.27910.0200
42022-04-14 15:15:44.6022022-04-14 14:15:45.45910.0129
52022-04-14 15:15:44.7782022-04-14 14:15:45.63510.0248
62022-04-14 15:15:44.9392022-04-14 14:15:45.93110.4694
72022-04-14 15:15:45.0752022-04-14 14:15:46.02310.0333
82022-04-14 15:15:45.2172022-04-14 14:15:46.07410.0191
92022-04-14 14:15:45.3092022-04-14 14:15:46.16710.0202
102022-04-14 14:15:45.4302022-04-14 14:15:46.28810.0230

  分析表可知,服务器接受到10条请求报文是在2秒内完成的,考虑到实际客户机发送报文是不同步的,可以认为服务器同时接收到10条请求报文;观察表的“时延”一列,客户机的10次连接均是在10s左右完成,即在这10次请求中,服务器从建立连接到返回响应报文的时间是一样的。因此可以得到结论:先到的报文并不会影响后到的报文与服务器建立连接,服务器是同时处理10次连接请求和回复的。

问题及解决方法

(1)服务器TCPServer.py报错:OSError: [WinError 10045] 参考的对象类型不支持尝试的操作
  服务器创建套接字时使用了错误的SOCK_DGRAM,正确的应该是使用SOCK_STREAM
(2)服务器TCPServer.py报错:TypeError: a bytes-like object is required, not ‘str’
  服务器回复的响应报文应该是字节流,所以需要将字符串类型转码为字节流。
(3)对于一次只能处理一个请求连接的服务器,客户机同时发出多个请求连接时,只有前几个请求正常得到服务器回复,后面的请求出现如下报错:TimeoutError: [WinError 10060] 由于连接方在一段时间内没有正确答复或连接的主机没有反应,连接尝试失败。
  服务器提供的可连接数过小,访问失败。解决方法是将可连接数调大,即将服务器程序中的serverSocket.listen(1) 改为serverSocket.listen(10)

代码提取

  相关代码可在https://github.com/Hny1216/Socket_TCP.git上提取。

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 C# WinForm 中,可以通过使用 TcpClient 类来实现与 Web 服务器TCP 连接通信。下面是一些实现步骤: 1. 引用命名空间:首先需要引用 System.Net.Sockets 命名空间,以便于使用 TcpClient 类。 2. 创建 TcpClient 对象:可以通过 TcpClient 类的构造函数来创建 TcpClient 对象。可以指定需要连接的 Web 服务器IP 地址和端口号。 3. 连接服务器:使用 TcpClient 类的 Connect() 方法来连接 Web 服务器。如果连接成功,可以通过 TcpClient 类的 Connected 属性来判断是否连接成功。 4. 发送数据:可以使用 TcpClient 类的 GetStream() 方法来获取网络流,然后使用网络流对象的 Write() 方法来发送数据。 5. 接收数据:可以使用 TcpClient 类的 GetStream() 方法来获取网络流,然后使用网络流对象的 Read() 方法来接收数据。 6. 关闭连接:使用 TcpClient 类的 Close() 方法来关闭连接。 下面是一个简单的示例代码: ``` using System; using System.Net.Sockets; namespace TcpClientDemo { class Program { static void Main(string[] args) { try { // 1. 创建 TcpClient 对象,指定需要连接的 Web 服务器IP 地址和端口号 TcpClient tcpClient = new TcpClient("127.0.0.1", 8888); // 2. 连接服务器 tcpClient.Connect("127.0.0.1", 8888); // 3. 发送数据 NetworkStream networkStream = tcpClient.GetStream(); string msg = "Hello, World!"; byte[] data = System.Text.Encoding.UTF8.GetBytes(msg); networkStream.Write(data, 0, data.Length); // 4. 接收数据 byte[] buffer = new byte[1024]; int length = networkStream.Read(buffer, 0, buffer.Length); string response = System.Text.Encoding.UTF8.GetString(buffer, 0, length); Console.WriteLine("Response: {0}", response); // 5. 关闭连接 tcpClient.Close(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } } ``` 当然,上述示例只是一个简单的演示,实际应用中需要根据具体需求进行修改和优化。同时,需要注意网络编程的异常处理和线程安全问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值