网络通信
使用网络的目的
把多方链接在一起, 进行数据传递;
网络编程就是, 让不同电脑上的软件进行数据传递, 即进程间通信;
ip 地址
ip 地址概念和作用
IP 地址是什么: 比如 192.168.1.1 这样的一些数字;
ip 地址的作用: 用来在电脑中 标识唯一一台电脑, 比如 192.168.1.1; 在本地局域网是唯一的.
网卡信息
查看网卡信息
- Linux:ifconfig
- Windows:ipconfig
ensxx: 用来与外部进行通信的网卡;
lo: 环回网卡, 用来进行本地通信的;
Linux 关闭 / 开启网卡: sudo ifconfig ensxx down/up
ip 和 ip 地址的分类
ip 分为 ipv4 和 ipv6
ip 地址分为:
A 类地址
B 类地址
C 类地址
D 类地址 -- 用于多播
E 类地址 -- 保留地址, 因 ipv6 诞生, 已无用
私有 ip
单播 -- 一对一
多播 -- 一对多
广播 -- 多对多
端口
ip: 标识电脑;
端口: 标识电脑上的进程 (正在运行的程序);
ip 和端口一起使用, 唯一标识主机中的应用程序, 进行统一软件的通信;
端口分类
知名端口
固定分配给特定进程的端口号, 其他进程一般无法使用这个端口号;
小于 1024 的, 大部分都是知名端口;
范围从 0~1023;
动态端口
不固定分配, 动态分配, 使用后释放的端口号;
范围 1024~65535;
socket
socket 的概念
socket 是进程间通信的一种方式, 能实现不同主机间的进程间通信, 即 socket 是用来网络通信必备的东西;
创建 socket
创建套接字:
- import socket
- soc = socket.socket(AddressFamily, Type)
函数 socket.socket 创建一个 socket, 该函数有两个参数:
Address Family: 可选 AF_INET(用于 internet 进程间通信) 和 AF_UNIX(用于同一台机器进程间通信);
Type: 套接字类型, 可选 SOCK_STREAM(流式套接字, 主用于 TCP 协议)/SOCK_DGRAM(数据报套接字, 主用于 UDP 套接字);
创建 tcp 套接字
- import socket
- soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- ...
- soc.close()
创建 udp 套接字
- import socket
- soc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- ...
- soc.close()
- udp
udp 使用 socket 发送数据
在同一局域网内发消息;
如果用虚拟机和 Windows, 要用桥接模式, 确保在同一局域网内;
- import socket
- def main():
- # 创建一个 udp 套接字
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- # 使用套接字收发数据
- udp_socket.sendto(b"hahaha", ("193.168.77.1", 8080))
- # 关闭套接字
- udp_socket.close()
- if __name__ == "__main__":
- main()
udp 发送数据的几种情况:
在固定数据的引号前加 b, 不能使用于用户自定义数据;
用户自定义数据, 并进行发送, 使用. encode("utf-8") 进行 encode 编码
用户循环发送数据
用户循环发送数据并可以退出
只贴出最后一种情况, 即完整代码
- import socket
- def main():
- # 创建一个 udp 套接字
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- while 1:
- # 从键盘获取要发送的数据
- send_data = input("请输入你要发送的数据:")
- if send_data == "exit":
- break
- # 使用套接字收发数据
- udp_socket.sendto(send_data.encode("utf-8"), ("193.168.77.1", 8080))
- # 关闭套接字
- udp_socket.close()
- if __name__ == "__main__":
- main()
udp 接收数据
接收到的数据是一个元组, 元组第一部分是发送方发送的内容, 元组第二部分是发送方的 ip 地址和端口号;
- import socket
- def main():
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- localaddr = ('', 8080)
- udp_socket.bind(localaddr) # 必须绑定自己电脑的 ip 和端口
- # 接收数据
- recv_data = udp_socket.recvfrom(1024)
- # recv_data 这个变量存储的是一个元组, 例如 (b'hahaha', ('192.168.77.1', 8888))
- recv_msg = recv_data[0]
- send_addr = recv_data[1]
- # print("%s 发送了:%s" % (str(send_addr), recv_msg.decode("utf-8"))) # Linux 发送的数据用 utf8 解码
- print("%s 发送了:%s" % (str(send_addr), recv_msg.decode("gbk"))) # Windows 发送的数据用 gbk 解码
- udp_socket.close()
- if __name__ == "__main__":
- main()
udp 接发数据总结
发送数据的流程:
创建套接字
发送数据
关闭套接字
接收数据的流程:
创建套接字
绑定本地自己的信息, ip 和端口
接收数据
关闭套接字
端口绑定的问题
如果在你发送数据时, 还没有绑定端口, 那么操作系统就会随机给你分配一个端口, 循环发送时用的是同一个端口;
也可以先绑定端口, 再发送数据.
udp 发送消息时自己绑定端口示例
- import socket
- def main():
- # 创建一个 udp 套接字
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- # 绑定端口
- udp_socket.bind(('192.168.13.1', 8080))
- while 1:
- # 从键盘获取要发送的数据
- send_data = input("请输入你要发送的数据:")
- if send_data == "exit":
- break
- # 使用套接字收发数据
- udp_socket.sendto(send_data.encode("utf-8"), ("193.168.77.1", 8080))
- # 关闭套接字
- udp_socket.close() # 按 ctrl+c 退出
- if __name__ == "__main__":
- main()
但应注意, 同一端口在同一时间不能被两个不同的程序同时使用;
单工, 半双工, 全双工
单工半双工全双工的理解
单工:
只能单向发送信息, 别人接收, 别人不能回复消息, 比如广播;
半双工:
两个人都能发消息, 但是在同一时间只能有一个人发消息, 比如对讲机;
全双工:
两个人都能发消息, 能同时发, 比如打电话;
udp 使用同一套接字收且发数据
- """socket 套接字是全双工"""
- import socket
- def main():
- udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- udp_socket.bind(('192.168.13.1', 8080))
- # 让用户输入要发送的 ip 地址和端口
- dest_ip = input("请输入你要发送数据的 ip 地址:")
- dest_port = int(input("请输入你要发送数据的端口号:"))
- # 从键盘获取要发送的数据
- send_data = input("请输入你要发送的数据:")
- # 使用套接字收发数据
- udp_socket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))
- # 套接字可以同时 收发数据;
- recv_data = udp_socket.recvfrom(1024)
- print(recv_data)
- # 关闭套接字
- udp_socket.close() # 按 ctrl+c 退出
- if __name__ == "__main__":
- main()
在这里体现不出来 socket 是全双工, 因为现在解释器只能按照流程, 一步一步走下去, 后面学习了进程线程协程就可以做到了.
tcp
tcp - 可靠传输
tcp 采取的机制
采用发送应答机制
超时重传
错误校验
流量控制和阻塞管理
tcp 与 udp 的区别
tcp 更安全可靠, udp 相对没那么安全可靠;
面向连接
有序数据传输
重发丢失的数据
舍弃重复的数据包
无差错的数据传输
阻塞 / 流量控制
tcp,udp 应用场景
tcp 应用场景: 下载, 发送消息
udp 应用场景: 电话, 视频直播等
tcp 客户端
tcp 客户端发送数据
- import socket
- def main():
- # 1. 创建 tcp 的套接字
- tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 2. 链接服务器
- tcp_socket.connect(('193.168.11.1', 8080))
- # 3. 发送 / 接收消息
- send_data = input("请输入你要发送的消息:")
- tcp_socket.send(send_data.encode("utf-8"))
- # 4. 关闭套接字
- tcp_socket.close()
- if __name__ == "__main__":
- main()
tcp 服务器
监听套接字, 专门用来监听的;
accept 会对应新创建的套接字, 当监听套接字收到一个请求后, 将该请求分配给新套接字, 由此监听套接字可以继续去监听了, 而新套接字则为该胡克段服务.
- import socket
- def main():
- # 创建 tcp 套接字
- tcp_service_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- tcp_service_socket.bind(('', 8080))
- # 让默认的套接字由主动变为被动
- tcp_service_socket.listen(128)
- # 等待客户端的链接
- new_client_socket, client_addr = tcp_service_socket.accept()
- print("链接的客户端地址为:", client_addr)
- # 接收客户端发送过来的请求
- recv_data = new_client_socket.recvfrom(1024)
- print(recv_data)
- # 给客户端回送消息
- new_client_socket.send("hahahah".encode("utf-8"))
- new_client_socket.close()
- tcp_service_socket.close()
- if __name__ == '__main__':
- main()
listen 里面的参数, 表示同时只允许 128 个链接访问.
QQ 不绑定端口的运行原理 - 扩展
udp 和 tcp 并用;
使用 QQ, 先登录, 登录后告诉腾讯服务器此 QQ 运行的端口, 发消息时, 通过腾讯服务器转发给另一个 QQ;
不绑定端口也有一个好处, 就是允许多开, 即一个电脑上可以运行多个 QQ;
recv 和 recvfrom 的区别
recvfrom 里面不仅有发过来的数据, 还有发过来数据的人的信息;
recv 里面就只有数据;
tcp 客户端服务端流程梳理
tcp 服务器流程梳理
创建服务器套接字
绑定本地信息
让默认的套接字由主动变为被动
等待客户端的链接, 堵塞
被客户端链接后, 创建一个新的客服套接字为客户端服务;
接收客户端发送的消息, 堵塞
接收客户端发送的消息后, 给客户端回消息
关闭客服套接字, 关闭服务端套接字
tcp 注意点
tcp 服务器一般情况下都需要綁定, 否则客户端找不到这个服务器.
tcp 客户端一般不绑定, 因为是主动链接服务器, 所以只要确定好服务器的 ip, port 等信息就好, 本地客户端可以随机.
tcp 服务器通过 listen 可以将 socket 创建出来的主动套接字变为被动的, 这是做 tcp 服务器时必须要做的.
当客户端需要链接服务器时, 就需要使用 connect 进行链接, udp 是不需要链接的而是直接发送, 但是 tcp 必须先链接, 只有链接成功才能通信.
当一个 tcp 客户端连接服务器时, 服务器端会有 1 个新的套接字, 这个套接字用来标记这个客户端, 单独为这个客户端服务.
liston 后的套接字是被动套接字, 用来接收新的客户端的链接请求的, 而 accept 返回的新套接字是标记这个新客户端的.
关闭 isten 后的套接字意味着被动套接字关闭了, 会导致新的客户端不能够链接服务器, 但是之前已经链接成功的客户端正常通信.
关闭 accept 返回的套接字意味着这个客户端已经服务完毕.
9. 当客户端的套接字调用 close 后. 服务器端会 recv 解堵塞, 并且返回的长度为 0, 因此服务器可以通过 返回数据的长度来区别客户端是否已经下线.
tcp 应用案例
示例 1 - 为一个用户办理一次业务:
- """可以理解为银行一个客服为排队的人员办理业务"""
- import socket
- def main():
- # 1. 创建 tcp 套接字
- tcp_service_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 2. 绑定本地信息
- tcp_service_socket.bind(('', 8080))
- # 3. 让默认的套接字由主动变为被动
- tcp_service_socket.listen(128)
- while 1:
- # 4. 等待客户端的链接
- new_client_socket, client_addr = tcp_service_socket.accept()
- print("链接的客户端地址为:", client_addr)
- # 接收客户端发送过来的请求
- recv_data = new_client_socket.recvfrom(1024)
- print(recv_data)
- # 给客户端回送消息
- new_client_socket.send("hahahah".encode("utf-8"))
- # 关闭套接字
- new_client_socket.close()
- tcp_service_socket.close()
- if __name__ == '__main__':
- main()
示例 2 - 为同一用户服务多次并判断一个用户是否服务完毕:
- """可以理解为银行一个客服为排队的人员办理业务"""
- import socket
- def main():
- # 1. 创建 tcp 套接字
- tcp_service_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 2. 绑定本地信息
- tcp_service_socket.bind(('', 8080))
- # 3. 让默认的套接字由主动变为被动
- tcp_service_socket.listen(128)
- while 1:
- # 4. 等待客户端的链接
- new_client_socket, client_addr = tcp_service_socket.accept()
- print("链接的客户端地址为:", client_addr)
- # 循环目的: 为同一个客户服务多次
- while 1:
- # 接收客户端发送过来的请求
- recv_data = new_client_socket.recvfrom(1024)
- print(recv_data)
- # 如果 recv 解堵塞, 那么有两种方式
- # 1. 客户端发了数据过来
- # 2. 客户端调用了 close
- if recv_data:
- # 给客户端回送消息
- new_client_socket.send("hahahah".encode("utf-8"))
- else:
- break
- # 关闭套接字
- new_client_socket.close()
- tcp_service_socket.close()
- if __name__ == '__main__':
- main()
示例 3-tcp 文件下载客户端和服务端:
文件下载客户端
- import socket
- def main():
- # 1. 创建套接字
- tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 2. 获取服务器的 ip,port
- dest_ip = input("请输入你要链接的服务器 ip:")
- dest_port = input("请输入你要链接的端口:")
- # 3. 链接服务器
- tcp_socket.connect((dest_ip, dest_port))
- # 4. 获取下载的文件名字
- want_file = input("请输入你要下载的文件:")
- # 5. 将文件名字发送到服务器
- tcp_socket.send(want_file.encode("utf-8"))
- # 6. 接收要下载的文件
- file_data = tcp_socket.recv(1024)
- # 7. 将接收文件的数据写入一个文件中
- if file_data:
- with open("[复件]" + want_file, "wb") as f:
- f.write(file_data)
- # 8. 关闭套接字
- tcp_socket.close()
- pass
- if __name__ == '__main__':
- main()
文件下载服务端
- import socket
- def send_file2client(new_socket, client_addr):
- # 1. 接受客户端发送过来的 要下载的文件名
- want_file = new_socket.recv(1024).decode("utf-8")
- print("客户端 %s 要接收的文件为:%s" % (str(client_addr), want_file))
- # 2. 读取文件数据
- file_data = None
- try:
- f = open(want_file, "rb")
- file_data = f.read()
- f.close()
- except Exception as e:
- print("你要下载的文件 %s 不存在" % want_file)
- # 3. 发送文件的数据给客户端
- if file_data:
- new_socket.send(file_data)
- def main():
- # 1. 创建套接字
- tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 2. 绑定本地信息
- tcp_socket.bind(('', 8080))
- # 3. 套接字被动接受 listen
- tcp_socket.listen(128)
- while 1:
- # 4. 等待客户端的链接 accept
- new_socket, client_addr = tcp_socket.accept()
- # 5. 调用函数发送文件到客户端
- send_file2client(new_socket, client_addr)
- # 7. 关闭套接字
- new_socket.close()
- tcp_socket.close()
- if __name__ == '__main__':
- main()
来源: https://www.cnblogs.com/yifchan/p/python-1-22.html