【socket】简单的网络数据收发程序
一、实验目的
通过本实验,学习采用 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 协议)以及一个端口号唯一确定。
三、实验步骤
0.环境介绍
python3.7;socket;pycharm
1.采用TCP进行数据发送的简单程序
1.1客户端
1.1.1建立socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
协议簇:AF_INET(TCP/IP – IPv4);类型:SOCK_STREAM(TCP)
1.1.2连接socket
连接socket需要提供一个tuple,包括host(主机名或者IP)和port(远程端口)
s.connect(('127.0.0.1', 1234))
127.0.0.1是回送地址,指本地机
1.1.3从socket获取信息
print(s.recv(1024))
从 socket 中接收数据,最多 1024 个字符
1.1.4 发送与接收数据
while True:
data = input('Please input data:')
s.send(data.encode('UTF-8'))
receiveBytes = s.recv(1024)
print('Server replied:\'{0}\' '.format(receiveBytes.decode('UTF-8')))
if data == 'exit':
print('Bye~')
break
利用send发送数据,recv接收数据。输入exit表示退出。
1.2服务器
1.2.1建立socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
协议簇:AF_INET(TCP/IP – IPv4);类型:SOCK_STREAM(TCP)
1.2.2设置socket选项
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
将SO_REUSEADDR标记为TRUE,操作系统会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口
,否则操作系统会保留几分钟该端口。防止socket server重启后端口被占用。
1.2.3绑定到一个端口
s.bind(('127.0.0.1', 1234))
127.0.0.1是回送地址,指本地机。端口选择和客户端连接socket的端口一致
1.2.4侦听连接
利用listen()函数进行侦听连接。该函数只有一个参数,其指明了在服务器实际处理连接的时候,允许有多少个未决(等待)的连接在队列中等待。
s.listen(5)
1.2.5处理连接
conn, addr = s.accept()
while True:
data = conn.recv(1024)
print('{0} client send data is \'{1}\''.format(addr,data.decode()))
if data.decode('UTF-8') == 'exit':
print('{0} connection close'.format(addr))
conn.send(bytes('Connection closed!', 'UTF-8'))
break
conn.send(bytes('Yes, I have received: \'{0}\''.format(data.decode()), "UTF-8"))
accept函数接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。接收TCP 客户的连接(阻塞式)等待连接的到来。
连接后在while循环里利用recv接收数据,若为exit
则关闭连接;利用send发送数据。
1.3 完整代码
SimpleSocketC.py:
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 1234))
except socket.error as msg:
print(msg)
exit(1)
while True:
data = input('Please input data:')
s.send(data.encode('UTF-8'))
receiveBytes = s.recv(1024)
print('Server replied:\'{0}\' '.format(receiveBytes.decode('UTF-8')))
if data == 'exit':
print('Bye~')
break
s.close()
SimpleSocketS.py:
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 1234))
s.listen(5)
except socket.error as msg:
print(msg)
exit(1)
print('Waiting connection...')
conn, addr = s.accept()
while True:
data = conn.recv(1024)
print('{0} client send data is \'{1}\''.format(addr,data.decode()))
if data.decode('UTF-8') == 'exit':
print('{0} connection close'.format(addr))
conn.send(bytes('Connection closed!', 'UTF-8'))
break
conn.send(bytes('Yes, I have received: \'{0}\''.format(data.decode()), "UTF-8"))
conn.close()
1.4 运行结果
1.4.1 首先运行服务器(SimpleSocketS.py)
此时服务器准备接收TCP 客户的连接(阻塞式),等待连接的到来
此时打开cmd,查看端口的占用,键入
Netstat -ano|findstr "127.0.0.1:1234"
再去看一下现在运行的python程序是不是能够对应到这个PID,键入
tasklist | findstr python
可以发现,PID为13076,就是刚刚运行的那个服务器程序
1.4.2 然后运行客户端(SimpleSocketC.py)
此时可以向服务器发送数据了
在发送数据之前,同样的先查看一下端口占用
可以看到这两个端口(1234与58441)即为服务端的进程与客户端的python的程序,且前者的源/目的地址,即为后者的目的/源地址。
1.4.3 发送数据与接收数据
1.4.4 退出通信并关闭连接
2.采用UDP进行数据发送的简单程序
2.1 客户端
使用UDP时,当socket被建立,程序调用的是SOCK_DGRAM,而不是SOCK_STREAM。
使用UDP时,也可不调用connect函数。发送数据用sendto函数,返回值是发送的字节数。接收数据用recvfrom函数,返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
2.2 服务器
服务器端使用UDP时,可以像使用TCP那样建立一个socket,设置选项,并调用bind函数。然而,不必使用listen和accept函数,仅仅使用recvfrom函数就可以了。这个函数会返回两个信息:收到的数据,以及发送这些数据的程序地址和端口号。因为UDP是无连接的协议,所以仅需要能发送一个答复,不需要像TCP那样有一个专门的socket和客户端相连
2.3 完整代码
SimpleUDPC.py:
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
except socket.error as msg:
print(msg)
exit(1)
while True:
data = input('Please input data:')
s.sendto(data.encode('UTF-8'),('127.0.0.1',1234))
if data == 'exit':
print('Bye~')
break
s.close()
SimpleUDPS.py:
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 1234))
except socket.error as msg:
print(msg)
exit(1)
print('Waiting connection...')
while True:
data, addr = s.recvfrom(1024)
print('{0} client send data is \'{1}\''.format(addr,data.decode()))
if data.decode('UTF-8') == 'exit':
print('{0} connection close'.format(addr))
break
s.close()
2.4 运行结果
3.多线程\线程池对比
当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建 立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户 端作出响应。 并行服务器:可以单独处理没一个连接,且不会产生干扰。并行服务器分为两种:一客 户一线程和线程池。 每个新线程都会消耗系统资源:创建一个线程将占用 CPU 周期,而且每个线程都自己 的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM 将保 存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状 态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时 间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额 外的线程实际上可能增加客户端总服务时间。 我们可以通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新 的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个 新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个 客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线 程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
3.1 多线程
3.1.1 TCPServerThread.py
利用threading实现多线程
import socket
import time
import threading
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 1234))
s.listen(5)
except socket.error as msg:
print(msg)
exit(1)
def deal_data(conn,addr):
print('Accept new connection from {0}'.format(addr))
conn.send(('Hi, Welcome to the server!').encode())
while True:
data = conn.recv(1024)
print('{0} client send data is \'{1}\''.format(addr, data.decode()))
time.sleep(1)
if data.decode('UTF-8') == 'exit':
print('{0} connection close'.format(addr))
conn.send(bytes('Connection closed!', 'UTF-8'))
break
conn.send(bytes('Yes, I have received: \'{0}\''.format(data.decode()), "UTF-8"))
conn.close()
print('Waiting connection...')
while True:
conn, addr = s.accept()
t = threading.Thread(target=deal_data,args=(conn,addr))
t.start()
3.1.2 TCPClientThread.py
客户端代码无须太大改动。为了模拟多用户,以下代码可复制多份,命名为TCPClient2Thread.py、TCPClient3Thread.py等等
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 1234))
except socket.error as msg:
print(msg)
exit(1)
print('\'{0}\' '.format(s.recv(1024).decode('UTF-8')))
while True:
data = input('Please input data:')
s.send(data.encode('UTF-8'))
receiveBytes = s.recv(1024)
print('Server replied:\'{0}\' '.format(receiveBytes.decode('UTF-8')))
if data == 'exit':
print('Bye~')
break
s.close()
3.1.3运行结果
首先运行TCPServerThread.py,然后运行TCPClientThread.py和TCPClient2Thread.py,输入数据
3.2 线程池
3.2.1 TCPServerPool.py
利用concurrent.futures中的ThreadPoolExecutor来实现线程池
import socket
import time
from concurrent.futures import ThreadPoolExecutor
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 1234))
s.listen(5)
except socket.error as msg:
print(msg)
exit(1)
def deal_data(conn,addr):
print('Accept new connection from {0}'.format(addr))
conn.send(('Hi, Welcome to the server!').encode())
while True:
data = conn.recv(1024)
print('{0} client send data is \'{1}\''.format(addr, data.decode()))
time.sleep(1)
if data.decode('UTF-8') == 'exit':
print('{0} connection close'.format(addr))
conn.send(bytes('Connection closed!', 'UTF-8'))
break
conn.send(bytes('Yes, I have received: \'{0}\''.format(data.decode()), "UTF-8"))
conn.close()
print('Waiting connection...')
with ThreadPoolExecutor(max_workers=5) as executor:
while True:
conn, addr = s.accept()
executor.submit(deal_data, conn, addr)
3.2.2 TCPClientPool.py
客户端代码无须太大改动。和TCPClientThread.py代码一致。代码可复制多份,命名为TCPClient2Pool.py、TCPClient3Pool.py等等
3.2.3运行结果
4.写一个简单的 chat 程序,并能互传文件,编程语言不限
https://www.jianshu.com/p/8c83da946a1a