网络编程
3.3.1 几个概念性知识
1. ip地址
- 标志网络中网络设备的地址,具有唯一性
- 分为ipv4和ipv6俩种协议和表现形式
- ipv4:点分十进制
- ipv6:冒号分16进制
- 局域网网段决定了网络设备的ip
- 查看ip地址的命令
- Linux/Mac:ifconfig(需要安装networktools)
- Windows:ipconfig
- ping可以测试设备之间的连通性,也可以测试设备是否连上网络
2. 端口
- 是传输数据的通道,每个端口有一个对应的端口号
- 端口号有1-65535个。
- 端口号是可以标志唯一的一个端口
- 端口号有2种
- 知名端口号:众所周知的端口号,这些端口固定分配给一些服务(0-1023)
- 动态端口号:开发应用程序使用的端口号,如果开发的时候没有设置,操作系统会在动态端口的范围内随机生成一个给程序使用(1024-65535)
- 当一个程序或者服务启用的时候会占用一个端口号,当结束的时候,会自动释放端口号(有时效性)
3. TCP
- 数据传输不能乱传的,发送之前还需要选择一个对应的传输协议,保证程序之间按照指定的传输规则进行数据的通信。
- TCP 的英文全拼(Transmission Control Protocol)简称传输控制协议,它是一种面向连接的、可靠的、基于字节流的传输层通信协议
- 特点
- 面向连接:通信双方必须先建立好连接才能传输数据,传输完必须断开连接来释放资源
- 可靠传输:
- 采用发送应答机制
- 超时重传:接受方不答的时候会重复发包
- 错误校验:接收方检验包发现包数据不等的时候会丢包等发送发重发
- 流量控制和阻塞管理
拓展
实际上传输数据不仅只有tcp协议,还有udp,属于传输层协议,传输数据有2种模型。
我们后面学习的http属于应用层协议。
tcp有个三次握手和四次挥手协议
4. socket
- 是进程之间通信的一个工具,进程之间想要进行网络通信数据传输需要基于这个socket
- socket接口唯一osi7层模型的应用层,是一组接口
- socket编程指的是利用soket接口来实现自己的业务和协议
- Socke接口属于软件抽象层,而sokcket编程却是标准的应用层开发
3.3.2 TCP网络应用程序开发
1. 客户端开发流程
- 创建套接字对象(socket)
- 和服务器端建立连接(connect())
- 发送数据(send())
- 接受数据(recv())
- 关闭连接(close())
2. 服务端开发流程
- 创建套接字对象(socket)
- 绑定端口(bind())
- 设置监听模式(listen())
- 等待客户建立连接(accept())
- 接受数据(recv())
- 发送数据(send())
- 关闭连接(close())
如果没有虚拟机或者服务器,可以在本地(127.0.0.1)建立连接,我们使用一个app叫网络调试助手来帮忙我们尝试开发。
3. 客户端代码实现示例
import socket
if __name__ == '__main__':
# socket早期是一个函数,后来转成了类,所以是小写,我们先创建一个客户端对象
# socket.socket(IP地址类型,传输层协议类型)
# socket.AF_INET 代表是 ipv4
# SOCK_STREAM 代表使用tcp传输控制协议
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# socket类有个实例方法connect(('ip地址',端口号))参数是一个元祖
tcp_client_socket.connect(("192.168.19.33", 8080))
while True:
send_msg = input("请输入要发送的信息:")
# 如果打exit则退出死循环 执行关闭连接
if send_msg == "exit":
break
# 传输数据要先转化成对应编码的二进制文件,Ubuntu那边网络调试助手编码格式固定为utf-8 我们就也这样使用
send_data = send_msg.encode("utf-8")
# 发送数据
tcp_client_socket.send(send_data)
# 接受数据,一次最大接受1024字节
recv_data = tcp_client_socket.recv(1024)
# 打印收到的数据
print(recv_data)
# 将收到的数据重新转码成可识别的字符串
recv_content = recv_data.decode('utf-8')
print("接受的数据为:", recv_content)
tcp_client_socket.close()
执行尝试
请输入要发送的信息:hi
b'hi'
接受的数据为: hi
请输入要发送的信息:test 000001
b'hi'
接受的数据为: hi
请输入要发送的信息:hhhhhh
b'000005'
接受的数据为: 000005
请输入要发送的信息:exit
Process finished with exit code 0
4. 服务端代码实现示例
import socket
if __name__ == '__main__':
# 同样的服务器端也是一样,先创建一个socket对象接口
# 其中socket.AF_INET代表ipv4 socket.SOCK_STREAM代表使用TCP协议
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 使用bind做一个端口绑定
tcp_server_socket.bind(('', 9222))
# 设置监听模式
# 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务与多个客户端,
# 不需要让客户端进行等待建立连接
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成
tcp_server_socket.listen(128)
# 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 1. 专门和客户端通信的套接字: service_client_socket
# 2. 客户端的ip地址和端口号: ip_port
service_client_socket, ip_port = tcp_server_socket.accept()
print("客户端的ip地址以及端口号为:", ip_port)
while True:
# 接受数据
recv_data = service_client_socket.recv(1024)
if recv_data:
print(recv_data)
recv_content = recv_data.decode('utf-8')
print(recv_content)
# 发送数据
send_msg = 'This is server'
service_client_socket.send(send_msg.encode('utf-8'))
else:
break
# 如果接受数据为空,表示客户端已经断开连接
service_client_socket.close()
注意点
- 服务器端需要2个以上的套接字,一个用来被动监听客户端的连接,一个用来主动接受客户端的信息和发送信息
- 只有客户端建立连接才会解阻塞
- 服务器套接字一般不关闭做死循环处理,一般是服务器更新的时候会关闭
- 如果没做端口复用,服务器端程序退出后端口不会立即退出,起因是tcp的四次挥手协议。会让服务器端等待一阵子再完全释放端口。
- 端口复用需要3个参数
- 参数1 需要复用的套接口
- 参数2 端口复用
- 参数3 是否开启端口复用 bool
- 如果listen后的套接字提前关闭,会导致新的客户端无法创建连接,但是已经连接的客户端不会被破坏通信。
- 如果客户端断开连接,接受信息会变成0,可以以此判断客户端是否断开链接来关闭套接字,客户端那边同理。
5. 案例:实现多线程的服务器端
我们之前设置的listen监听数约等于白设置,因为我们那段代码一次只能连接一个客户端,如果要连接多个客户端,我们必须实现多任务版本的,而因为传输数据是需要频繁实现io耗时,不适合多进程。故用多线程来实现。
具体实现步骤
- 编写一个TCP服务端程序,循环等待接受客户端的连接请求
- 当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞
- 把创建的子线程设置成为守护主线程,防止主线程无法退出。
以下是代码
import socket
import threading
def client_link(service_client_socket,ip_port):
# 循环接受客户端数据
while True:
recv_data = service_client_socket.recv(1024)
if recv_data:
print(f"ip地址为{ip_port[0]}客户发来消息:", recv_data.decode('utf-8'))
service_client_socket.send("请稍等".encode('utf-8'))
else:
# 如果接受数据为空,则确认客户端已经断开连接
print("客户端已经断开连接,客户对应ip为:", ip_port)
break
service_client_socket.close()
if __name__ == '__main__':
# 先创造服务器套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设定端口复用,如果客户断开连接则释放端口
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 设置固定端口
tcp_server_socket.bind(("", 9222))
# 设置监听连接数
tcp_server_socket.listen(128)
while True:
service_client_socket, ip_port = tcp_server_socket.accept()
print("客户端连接上了,客户的ip地址和端口为:", ip_port)
# 每当连接上一个新客户端,就给新客户端创建一个新线程
new_thread = threading.Thread(target=client_link, args=(service_client_socket, ip_port))
# 设置守护线程
new_thread.setDaemon(True)
# 设置启动该子线程
new_thread.start()
我们执行代码
可以同时在虚拟机里多开几个网络调试助手
客户端连接上了,客户的ip地址和端口为: ('192.168.19.33', 53310)
客户端连接上了,客户的ip地址和端口为: ('192.168.19.33', 53312)
ip地址为('192.168.19.33', 53310)客户发来消息: kki
ip地址为('192.168.19.33', 53312)客户发来消息: kkp
ip地址为('192.168.19.33', 53312)客户发来消息: kkp
ip地址为('192.168.19.33', 53310)客户发来消息: kki
客户端已经断开连接,客户对应ip为: ('192.168.19.33', 53312)
客户端已经断开连接,客户对应ip为: ('192.168.19.33', 53310)
注意点
6. 案例:点对点机器人
实现局域网内的点对点聊天机器人程序。
使用TCP协议编写 socket 程序,分别实现消息的发送端和接收端
服务端记录客户端发送的消息,并进行随机回复
当客户端发送Bye时结束聊天
服务端
import socket
import threading
import random
bot_reply_list = ['Hello', 'Hi', "How Are You", "Sorry"]
def chat_save(data, ip_port):
with open(f'{ip_port}.txt', 'a', encoding='utf-8')as f:
f.write(data)
def client_chat(service_tcp_socket, ip_port):
while True:
recv_data = service_tcp_socket.recv(1024)
if recv_data:
recv_msg = recv_data.decode('utf-8')
chat_save(recv_msg, ip_port)
print(f"用户{ip_port}的发言已记录")
if recv_msg == 'bye':
break
send_data = bot_reply_list[random.randint(0, len(bot_reply_list)-1)].encode('utf-8')
service_tcp_socket.send(send_data)
else:
break
print(f"与用户{ip_port}断开连接")
service_tcp_socket.close()
def main():
# 创建一个监听用的套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 实现端口复用
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 设置固定端口
tcp_server_socket.bind(("", 9222))
# 设置监听连接数
tcp_server_socket.listen(128)
while True:
# 确认握手建立连接
service_tcp_socket, ip_port = tcp_server_socket.accept()
print(f"{ip_port}用户与您创立了连接")
# 为此连接创建一个新的线程
new_chat_thread = threading.Thread(target=client_chat, args=(service_tcp_socket, ip_port))
new_chat_thread.setDaemon(True)
new_chat_thread.start()
if __name__ == '__main__':
main()
客户端
import socket
if __name__ == '__main__':
# socket早期是一个函数,后来转成了类,所以是小写,我们先创建一个客户端对象
# socket.socket(IP地址类型,传输层协议类型)
# socket.AF_INET 代表是 ipv4
# SOCK_STREAM 代表使用tcp传输控制协议
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# socket类有个实例方法connect(('ip地址',端口号))参数是一个元祖
tcp_client_socket.connect(("127.0.0.1", 9222))
print("您已经连接上机器人")
while True:
send_msg = input("请输入要发送的信息:")
# 如果打exit则退出死循环 执行关闭连接
if send_msg == "exit":
break
# 传输数据要先转化成对应编码的二进制文件
send_data = send_msg.encode("utf-8")
# 发送数据
tcp_client_socket.send(send_data)
# 接受数据,每次最大接受1024字节
recv_data = tcp_client_socket.recv(1024)
if recv_data:
# 打印收到的数据
print(recv_data)
# 将收到的数据重新转码成可识别的字符串
recv_content = recv_data.decode('utf-8')
print("机器人:", recv_content)
else:
break
tcp_client_socket.close()
7. 理解 socket.send() sokcket.recv()
- socket.send() -> 缓冲区 -> 网卡 -> 网络
- 网络 -> 网卡 -> 缓冲区 -> socket.recv(1024)
实际发送和接受操作的都是缓存区,所以不会说过多的就读取不到了。但是缓存区也有限制,如果存放的内容超出缓冲区会会异常。