计算机网络实验2:网络编程实验 python3

代码地址:网络编程实验

实验目的

通过本实验,学习采用 Socket(套接字)设计简单的网络数据收发程序,理解应用数据 包是如何通过传输层进行传送的。

实验内容

Socket(套接字)是一种抽象层,应用程序通过它来发送和接收数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。一个 socket 允许应用程序添加到网 络中,并与处于同一个网络中的其他应用程序进行通信。一台计算机上的应用程序向 socket 写入的信息能够被另一台计算机上的另一个应用程序读取,反之亦然。

不同类型的 socket 与不同类型的底层协议族以及同一协议族中的不同协议栈相关联。 现在 TCP/IP 协议族中的主要 socket 类型为流套接字(sockets sockets)和数据报套接字 (datagram sockets)。流套接字将 TCP 作为其端对端协议(底层使用 IP 协议),提供了一个可信赖的字节流服务。一个 TCP/IP 流套接字代表了 TCP 连接的一端。数据报套接字使用 UDP 协议(底层同样使用 IP 协议),提供了一个"尽力而为"(best-effort)的数据报服务, 应用程序可以通过它发送最长 65500 字节的个人信息。一个 TCP/IP 套接字由一个互联网地址,一个端对端协议(TCP 或 UDP 协议)以及一个端口号唯一确定。

实验流程

采用 TCP 进行数据发送的简单程序
服务端

在设定了端口号之后,获取 TCP 对应的链接,并且将端口号和 TCP socket 绑定,并设定监听值:

serverPort = 8887
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(('', serverPort))
serverSocket.listen(1)

之后便是一个while循环,接受客户端发来的链接,并且调用 send 和 recv 接口进行信息的传递:

while True:
    connectionSocket, addr = serverSocket.accept()
    sentence = connectionSocket.recv(10).decode()
    capitalizedSentence = sentence.upper()
    connectionSocket.send(capitalizedSentence.encode())
    connectionSocket.close()
客户端

首先在客户端Client声明端口:

serverName = '49.234.84.130'
serverPort = 8887

之后通过socket提供的接口获取 TCP 对应的socket数据结构:

clientSocket = socket(AF_INET, SOCK_STREAM)

接下来就是与对应的客户端发起连接请求,连接完成后,即可进行数据的收发:

clientSocket.connect((serverName, serverPort))
sentence = input('Input lowercase sentence:')
clientSocket.send(sentence.encode())
modifiedSentence = clientSocket.recv(10)
print('From Server:', modifiedSentence.decode())
clientSocket.close()
采用 UDP 进行数据发送的简单程序

这一部分整体与TCP相似,之不过调用的接口为 UDP 的接口,而且没有建立连接的过程

使用 SOCK_DGRAM 获取 UDP 对应的 socket

clientSocket = socket(AF_INET, SOCK_DGRAM)

之后剩余部分基本类似

服务端
from socket import *
serverPort = 8888
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind(('', serverPort))
print("The server is already to receive")
while True:
    message, clientAddress = serverSocket.recvfrom(2048)
    modifiedMessage = message.decode().upper()
    serverSocket.sendto(modifiedMessage.encode(), clientAddress)
客户端
from socket import *
serverName = '49.234.84.130'
serverPort = 8888
clientSocket = socket(AF_INET, SOCK_DGRAM)
message = input('Input lowercase sentence:')
clientSocket.sendto(message.encode(), (serverName, serverPort))
modifiedMessage, serverAddress = clientSocket.recvfrom(2048)
print(modifiedMessage.decode())
clientSocket.close()

在这里可以看到,UDP 使用的是 sendto 和 recvfrom,这里 sendto 的第二个参数为 IP 和 Port 组成的元组

多线程\线程池对比

这一部分则是对服务器开启多线程,用来实现并发的效果,同时接受多个客户端的连接并且都为其提供服务

多线程

对于多线程,使用的为 python 提供的 threading 库

服务端

服务端与上述程序的最大区别在于,while循环内没接收的一个连接,则开启一个线程,并且启动线程,为当前连接的客户端进行服务:

while True:
    connectionSocket, addr = serverSocket.accept()
    t1 = Thread(target=TCP_Link, args=(connectionSocket, ))
    t1.start()

构造的Thread类传入两个参数,一个是启动的函数,另一个则为函数的参数,以可迭代的元组形式给出。

客户端与上述的 TCP 相似,不再赘述

线程池

线程池则是应用 concurrent 的 futures 包内的 ThreadPoolExecutor 来实现

服务端

与上面的多线程遇到一个则开启一个线程不同,线程池则是提前分配几个线程,放入线程池中,需要时则取出;如果可取的线程数量为空,那么则阻塞当期的连接(在实验结果部分给出了验证测试):

with ThreadPoolExecutor(max_workers=2) as t:
    while True:
        connectionSocket, addr = serverSocket.accept()
        print("Connected by IP: " + addr[0] + " Port: %d" % addr[1])
        t1 = t.submit(TCP_Link, connectionSocket, addr)

这里可以看到,对于 ThreadPoolExecutor 设置了最大的连接数量为 2 。

写一个简单的 chat 程序,并能互传文件,编程语言不限

考虑需要解决的问题:

  1. 传输的信息大小超过可接受的缓存
  2. 如何实现文件互传
  3. 如何实现两个客户端之间的通信
  4. 客户端的IO交互
Q1:传输的信息大小超过可接受的缓存

对于第一个问题,则是将信息分段发送,每次分为缓冲区可接受大小的最大值,再进行发送,最后发送单独的 ‘EOF’ 来作为信息的结尾。这样,在接收方未接收到 ‘EOF’ 字段时,则循环接受发送的信息,并且拼接为原始的字符串,接收到 ‘EOF’ 则停止,进行输出。

def msg_send(content, clientSocket):
    clientSocket.send(("msg").encode())  # 首先发出消息表明当前为消息的传输
    time.sleep(0.1)
    sentences = []
    while len(content) > 0:
        sentences.append(content[:min(len(content), 1024)])
        content = content[min(len(content), 1024):]
    for sentence in sentences:
        clientSocket.send(sentence.encode())
        time.sleep(0.1)
    clientSocket.send("EOF".encode())  # 最后发送EOF
    time.sleep(0.1)
    return

以上为文本的发送部分,将输入的信息分割为各个字符串。但是这样还会存在一个问题:会出现沾包的现象。这个问题的解决方式有两点:1.等待接收方返回一个信息如 ‘ACK’ 再进行下次发送 2.间隔一段时间再进行下次的发送。这里我选择了第二种方式,因为考虑到之后对于客户端,需要开两个线程分别进行收和发的处理,所以如果采用第一种方式,则会导致竞争的出现:两个线程都在接受。

所以在上面的代码中,可以看到,加入了time.sleep(0.1)语句以防止沾包。另外还有遇到的第二个问题,如果缓冲区较大,那么可能对于接受的信息未完全接受则输出,因为对于recv函数,如果缓冲区有数据则会取出,并不会等待,所以缓冲区的大小不宜过大。另外一种解决方式是在recv加入第二个参数(具体是啥参数我忘了,因为我也没用,我也是云的),等待缓冲区满才进行取数据。

def msg_recv(clientSocket):
    sentences = ""
    sentence = clientSocket.recv(1024).decode()
    while sentence != "EOF":  # 未接收到EOF
        sentences += sentence
        sentence = clientSocket.recv(1024).decode()
    print(sentences)
    return

在 line 4 则是进入了循环接受部分,直到接受了 ‘EOF’ ,最后将接受的文本拼接输出

Q2:如何实现文件互传

实现了上述的信息传输问题,那么文件的互传也比较好解决,只需要把文件的内容读出,作为信息传输即可,这里具体的实现,是在传输文件信息之前,先传输文件名,使得接收方在本地创建对应的文件,以便后续的写入

def file_send(filename, clientSocket):
    try:
        file = open(filename, encoding='utf-8')
    except FileNotFoundError:
        print("File Not Found, please check your input.")
        return "error"
    sentences = []
    content = file.read()
    clientSocket.send(("file:" + filename).encode())  # 首先发出消息表明当前为文件的传输
    time.sleep(0.1)
    while len(content) > 0:
        sentences.append(content[:min(len(content), 1024)])
        content = content[min(len(content), 1024):]
    for sentence in sentences:
        clientSocket.send(sentence.encode())
        time.sleep(0.1)
    clientSocket.send("EOF".encode())
    time.sleep(0.1)
    return

这里在打开文件时进行了异常处理,如果文件不存在则会返回错误信息。

def file_recv(clientSocket, filename):
    file = open(filename, "w", encoding='utf-8')
    sentence = clientSocket.recv(1024).decode()
    while sentence != "EOF":
        file.write(sentence)
        sentence = clientSocket.recv(1024).decode()
    print("File Receive Success.")
    return

接受部分则于上面的相同,只不过多了打开文件的步骤。

Q3:如何实现两个客户端之间的通信

由于每个 TCP 连接都维护一个五元组,用来标识唯一的一个 TCP 连接,所以可以通过这一点来进行实现。当客户端连接到服务器之后,需要输入当前客户端的名称,输入之后在服务端进行检查,如果不存在重复则合法,放入到服务端维护的字典中,字典的值即打开的 TCP 连接。在客户端信息发送时,应当输入接收方的名称,如果存在,则先向服务端发送信息,服务器暂存,再将接收方信息解析,取出对应的 TCP 连接,由服务端发送到对应的客户端。

global conn_pool  # 维护的连接
conn_pool = dict()
conn_pool["Server"] = serverSocket
with ThreadPoolExecutor(max_workers=2) as t:
    while True:
        connectionSocket, addr = serverSocket.accept()
        name = connectionSocket.recv(1024).decode()
        while conn_pool.get(name):
            connectionSocket.send("AlreadyExist!".encode())
            name = connectionSocket.recv(1024).decode()
        connectionSocket.send("OK".encode())
        conn_pool[name] = connectionSocket  # 将连接放入字典
def send_proc(parse, from_link, to_link):
    global conn_pool
    connectionSocket = conn_pool[to_link]

在服务器的发送进程内,取出接收方的TCP连接,方便之后的发送

最后在客户端退出后,将其连接删除:

    del conn_pool[name]
    print("%s connection closed" % name)
    connectionSocket.close()
Q4:客户端的IO交互

客户端的IO交互一直是一个让我很为烦恼的地方,在没有实现界面的情况下,所有的输入输出都会从命令行进行,这就导致显示非常乱,并不能给出很好的指引。为了解决这个问题,我将接收到的信息的输出重定向到文件中,这种方式牺牲了查看的便捷性,但是却能显著的提高IO的展示效果。通过全局打开一个以当前客户端名称命名的文件,将收到的信息和回馈输入到该文件中,并在每次接收到数据就进行 flush ,使得信息能够及时的反馈。

fw = open(name + ".output", "w", encoding='utf-8')
fw.write("It's %s" % name)
fw.flush()

在接受的处理中,进行重定向:

def msg_recv(clientSocket):
    sentences = ""
    sentence = clientSocket.recv(1024).decode()
    while sentence != "EOF":
        sentences += sentence
        sentence = clientSocket.recv(1024).decode()
    print(sentences, file=fw)  # 重定向到fw
    fw.flush()  # 刷新缓冲区
    return

到这里,实验4的主要部分已经完成了,将上述的逻辑整合到之前的代码中即可完成这一部分的操作。还需注意的是,在客户端中要开启多线程,使得收发进程同时运行,并且阻塞主线程。

服务端
# 服务端收
def recv_proc(connectionSocket):
    link = connectionSocket.recv(1024).decode()
    link = link.split(":")
    from_link = link[0]
    to_link = link[1]
    global conn_pool
    if not conn_pool.get(to_link) and to_link != "Server":
        users = ""
        for k, v in conn_pool.items():
            users += k + ' '
        connectionSocket.send("Message From Server".encode())
        time.sleep(0.1)
        connectionSocket.send(("NotExist%s" % users).encode())
        return 2, "", "", ""
    sentences = ""
    parse = connectionSocket.recv(1024).decode()
    # 根据不同的接受的小心进行解析,判断是文件、退出标志或者是信息
    if parse[:4] == "file":
        sentences = parse
        file_recv(connectionSocket, parse[5:])
    elif parse == ":q":
        sentences = parse
        return 1, sentences, from_link, to_link
    elif parse == "msg":
        sentences = msg_recv(connectionSocket)
    return 0, sentences, from_link, to_link
# 服务端发
def send_proc(parse, from_link, to_link):
    global conn_pool
    if parse == ":q":
        conn_pool[from_link].send(("Message From Server").encode())
        time.sleep(0.1)
        conn_pool[from_link].send(parse.encode())
        return 1

    connectionSocket = conn_pool[to_link]
    print("From %s Send to %s" % (from_link, to_link))

    if to_link != "Server":
        connectionSocket.send(("Message From %s" % from_link).encode())
    else:
        print("Receive from %s: " % name, end='')
    if parse[:4] == "file":
        if to_link != "Server":
            file_send(parse[5:], connectionSocket)
        print("File Transfer Success.")
    else:
        if to_link != "Server":
            msg_send(parse, connectionSocket)
        else:
            print(parse)
    return 0
# 为每个连接开启的线程
# 服务端的处理逻辑为先进行接受,再将数据发送
def main_proc(connectionSocket, name):
    global conn_pool
    while True:
        fg, parse, from_link, to_link = recv_proc(connectionSocket)
        if fg == 2:
            continue
        fg = send_proc(parse, from_link, to_link)
        if fg == 1:
            break

    del conn_pool[name]
    print("%s connection closed" % name)
    connectionSocket.close()

serverPort = 65535
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(('', serverPort))
serverSocket.listen(1)
global conn_pool
conn_pool = dict()
conn_pool["Server"] = serverSocket
print("READY")
with ThreadPoolExecutor(max_workers=5) as t:
    while True:
        connectionSocket, addr = serverSocket.accept()
        name = connectionSocket.recv(1024).decode()
        while conn_pool.get(name):
            connectionSocket.send("AlreadyExist!".encode())
            name = connectionSocket.recv(1024).decode()
        connectionSocket.send("OK".encode())
        conn_pool[name] = connectionSocket
        print("Connected by %s name %s" % (str(addr), name))
        task = t.submit(main_proc, connectionSocket, name)

以上的具体发送和接受处理均为上面展示的模块,完整代码文章开头详见github的repo。

客户端

这一部分大体与上面相似,主要涉及到的难点也都在上面的问题中进行了阐述,主要展示开启多线程的部分

serverName = '49.234.84.130'
serverPort = 65535
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((serverName, serverPort))

name = input("input client name: ")
clientSocket.send(name.encode())
reply = clientSocket.recv(1024).decode()
while reply == "AlreadyExist!":
    print(reply)
    name = input("input client name: ")
    clientSocket.send(name.encode())
    reply = clientSocket.recv(1024).decode()

fw = open(name + ".output", "w", encoding='utf-8')
fw.write("It's %s" % name)
fw.flush()

global exist
exist = False

send_task = Thread(target=send_proc, args=(clientSocket, name))
recv_task = Thread(target=recv_proc, args=(clientSocket, ))
send_task.start()
recv_task.start()
send_task.join()
recv_task.join()

fw.close()
clientSocket.close()

这里在处理完用户名的输入之后,进行文件的打开,开启收发的线程。

此外,这里还有一个涉及到的难点:当接收方的用户名不存在时,应当结束后续的发送任务,但是这里是由收线程控制,所以设置了全局变量,当不存在时,收线程将其设置为True,对于发线程,当接受完用户名的输入之后,进行 time.sleep() 的阻塞,用来等待收线程的完成。(当我等待验收在写这份报告的时候,我突然想到:不是有个叫信号量的东西么,在发线程的时候进行V操作,收线程接受完对于用户名的校验则进行P操作,此时发线程则可以继续运行,完成线程同步。操作系统老师,对不起…)

def recv_proc(clientSocket):
    fg = True
    while fg:
        from_whom = clientSocket.recv(1024).decode()
        parse = clientSocket.recv(1024).decode()
        if parse[:4] == "file":
            print('\n%s' % from_whom, end=': ', file=fw)
            fw.flush()
            file_recv(clientSocket, parse[5:])
        elif parse == ":q":
            fg = False
        elif parse[:8] == "NotExist":  # 用户名不存在
            print('%s' % from_whom, end=': ')
            print("User Not Found")
            print("User List: %s" % parse[8:])
            global exist
            exist = True  # 设置为True
        else:
            print('\n%s' % from_whom, end=': ', file=fw)
            fw.flush()
            msg_recv(clientSocket)
    return

def send_proc(clientSocket, name):
    fg = True
    while fg:
        to_name = input("to which user: ")
        clientSocket.send((name + ":" + to_name).encode())
        time.sleep(0.5)  # 阻塞
        global exist
        if exist:
            exist = False
            continue  # 不存在,暂停当前过程
        parse = input("input \'help\' for help: ")
        …………

实验结果

采用 TCP 进行数据发送的简单程序
服务端

在这里插入图片描述

客户端

在这里插入图片描述

采用 UDP 进行数据发送的简单程序
服务端

在这里插入图片描述

客户端

在这里插入图片描述

多线程\线程池对比
多线程
服务端

在这里插入图片描述

客户端

在这里插入图片描述

可以多个客户端同时连接

线程池
max_workers的验证

首先开启四个线程(这里,最大的线程数量为2):

在这里插入图片描述

由于未阻塞主线程,所以在服务端输出了四个线程连接的信息。

在这里插入图片描述

可以看到,前两个线程,都能够正常得进行数据的收发,但是第三个线程,数据发送后却收不到服务器返回地消息,说明被阻塞,第四个线程也是。

将第一个线程发送exit结束连接后,再次切换到第三个线程,可以看到收到了返回地值并且打印在终端,并且第四个线程依旧处于阻塞状态:

在这里插入图片描述

这时在服务器可以看到第一个线程退出:

在这里插入图片描述

再关闭一个线程,就可以看到第四个线程恢复:

在这里插入图片描述

写一个简单的 chat 程序,并能互传文件,编程语言不限

启动服务器后,启动两个Client程序,一个所在的目录包含如下文件,命名为A:

在这里插入图片描述

另一个包含如下文件,命名为B:

在这里插入图片描述

在这里插入图片描述

此时,服务器端的信息为:

在这里插入图片描述

并且A创建文件A.output,B创建文件B.output

在这里插入图片描述

首先,A向B发送信息Hello,再发送World:

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

可以在B的文件中看到接收到了发送的消息,同时服务器端的记录为:

在这里插入图片描述

之后B向A发送Helloworld,从左至右依次为B的输入,A的接受,服务器端的记录:

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

下面进行文件传输的测试,从A向B传输in文件,其内容为:

在这里插入图片描述

发送:

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

从左至右依次为A的输入,A的输出:显示文件传输成功,B的输出:显示从A接受到文件

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

在B程序的位置可以看到接收到了in文件,时间为13:44(当前时间),文件内容也与发送的一致。

从B向A传输文件README.md,内容为:

在这里插入图片描述

发送:

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

可以看到在A位置接收到了文件,时间为13:47,内容于发送前一致。

在这里插入图片描述

可以看到文件传输以及消息的发送都能正常完成互传。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值