一. socket简介
● 不同电脑间要进行通信,通过网络层的ip地址可以唯一标识确认在网络中的主机,通过传输层的协议(tcp/udp)和端口可以唯一标识确认主机中的应用进程,那么拥有这些信息后即可在不同主机间也能找到想要发送的进程对象。
● socket是进程间通信的一种方式,它最大的特长是能实现不同主机中的进程间通信,我们网络上的服务大多都是基于socket来完成通信,例如email,QQ聊天,浏览网页等。
● 实现网络功能最核心最原始的方式就是socket通信
·
1.创建socket
import socket
# AddressFamily表示协议族,如socket.AF_INET(IPV4协议族)
# Type表示协议类型,如UDP、socket.SOCK_STREAM(TCP协议)
socket.socket(AddressFamily, Type)
·
1.1. 创建tcp socket(tcp套接字)
import socket
# 创建tcp的套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 不用的时候关闭
s.close()
·
1.2.创建udp socket(udp套接字)
import socket
# 创建udp的套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 不用的时候关闭
s.close()
·
二. udp面向无连接的通信过程
·
2.1 udp socket 发送数据
● 发送数据时用 .sendto(”要发送的数据的字节流形式“,("ip地址", 端口号))
● 这里要发送的数据需改变编码方式,固定的字符串在前面加 b
即可,形参用 .encode("utf-8")
进行编码
● 当没有绑定发送端的ip地址和端口号时,在发送数据时操作系统会随机分配一个端口给发送端
·
示例代码:
import socket
# 创建IPV4协议族(socket.AF_INET),使用UDP(socket.SOCK_DGRAM)协议的套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定发送端的IP地址和端口号,可写可不写,不写的话操作系统随机分配一个端口号
udp_socket.bind(("", 4567))
# 套接字发送信息,b表示发送的是字节流,发送的内容,ip地址和端口号以元组的形式
udp_socket.sendto(b"hello world", ("172.22.20.14", 8080))
s = input("请输入要发送的字符串:")
# 发送的内容前面无法加b进行修饰时,改变编码方式即可
udp_socket.sendto(s.encode("utf-8"), ("172.22.20.14", 8080))
udp_socket.close()
实验结果:
·
2.2 udp socket 接收数据
● 接收数据时,要先用bind(ip地址和端口号元组的形式)
方法绑定接收端的IP地址和端口号
● 接收数据用 .recvfrom(限制最大接收多少数据)
方法进行接收
● 当接收来自Windows的发来的消息时需要解码,因为Windows端发送中文时以 gbk
方式编码,在Ubuntu下接收该数据时也需要用 gbk
方式进行解码(decode("gbk")
),为Ubuntu认得的字符串形式。如果接收来自Linux系统下发来的消息时用utf-8
方式进行解码(decode("utf-8")
)即可。
示例代码:
import socket
def main():
# 创建udp套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置ip地址(空表示本机ip)和端口号
recv_addr = ("", 7879)
# 绑定ip地址和端口号
udp_socket.bind(recv_addr)
while True:
# 接收数据,设置最大接收量为1024个字节(1kb)
recv_data = udp_socket.recvfrom(1024)
# 将附有发送端ip地址和端口号的元组强制转换字符串类型,将发送的数据用gbk方式>进行解码输出
print("%s : %s" % (str(recv_data[1]), recv_data[0].decode("gbk")))
# 关闭套接字
udp_socket.close()
实验结果:
·
2.3 同一个套接字可以同时接收同时发送(如同电话座机)
单工:只能做单个任务,接收或者发送,如收音机
半双工:拥有接收和发送的功能,但在同一时刻只允许做一件事,如对讲机
全双工:拥有接收和发送的功能,在同一时刻可以同时做到接收和发送,如电话
套接字属于全双工(通过多任务可实现)
UDP聊天室代码实例:
# -*- coding:utf-8 -*-
import socket
def send_socket(s):
s1 = input("请输入ip地址:")
s2 = int(input("请输入端口号:"))
s3 = input("请输入要发送的内容:")
s.sendto(s3.encode("utf-8"), (s1, s2))
def recv_socket(s):
recv_data = s.recvfrom(1024)
print("="*25)
print("发送端ip:{}".format(recv_data[1][0])) # 这里recv_data的内容格式为 (b"内容", ("ip",port))
print("发送端port:{}".format(recv_data[1][1]))
print("接收到的内容:{}".format(recv_data[0].decode("utf-8"))) # 这里recv_data[0]的内容格式为 b"数据" 为字节流,将字节流转换为字符串格式为str(bytes, encoding="utf-8")
print("="*25)
def main():
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(("", 7788))
print("{:*^50}".format("欢迎来到聊天室!"))
while True:
print("1发送数据")
print("2接收数据")
print("0退出聊天室")
index = input("请输入要选择的功能:")
if index == "1":
send_socket(udp_socket)
input("输入回车程序继续!")
elif index == "2":
recv_socket(udp_socket)
input("输入回车程序继续!")
elif index == "0":
break
else:
print("输入有误,请重新输入:")
input("输入回车程序继续!")
if __name__ == "__main__":
main()
实验结果:
·
udp的通信模型
·
·
·
三. tcp 面向连接的通信过程
·
3.1 tcp socket 建立客户端,发送数据
● 发送数据用 .send(数据字节流形式)
● 建立客户端流程:创建套接字👉建立连接👉发送数据👉关闭套接字
·
示例代码:
# -*- coding:utf-8 -*-
import socket
def main():
# 创建TCP套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定ip地址和端口号(客户端可绑可不绑)
tcp_socket.bind(("192.168.220.137", 63214))
# 连接服务器的ip地址和端口号
tcp_socket.connect(("192.168.220.137", 8080))
# 发送数据,将数据以字节流形式发出
send_data = input("要发送的数据:")
tcp_socket.send(send_data.encode("utf-8"))
# 关闭套接字
tcp_socket.close()
if __name__ == "__main__":
main()
实验结果:
·
3.2 tcp socket 建立服务器端,接收并回发消息
● 建立服务器端的流程:创建服务器套接字 👉 绑定ip地址和端口信息 👉 用.listen(128)
让服务器套接字由主动改为被动 👉 设置服务器套接字为监听状态 .accept()
,当监听到客户端发送连接后,返回两个数据:一个是为这个客户端专门服务的新套接字,另一个是客户端的地址和端口信息 👉 新套接字用 .recv(最大接收字节数)
接收消息 👉新套接字用 .send(数据的字节流形式)
回送一部分数据给客户端 👉 关闭开启的所有套接字
● 将服务器套接字从主动改为被动的listen(128)
方法,128表示在同一时刻服务器套接字可以监听服务大量的客户端,而具体数值的大小与操作系统本身有关,默认为128
● 当开始创建的服务器套接字由主动改为被动后,唯一的作用就是监听客户端连接并创建套接字返回客户端ip和端口信息,每监听到一个客户端发起连接,都会创建一个新的套接字用来专门服务对应的客户端
● 新的套接字专门接收客户端发来的数据信息并可以回送部分数据给客户端
·
示例代码:
# -*- coding:utf-8 -*-
import socket
def main():
# 创建服务器套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地ip和端口号
tcp_socket.bind(("", 7890))
# 改服务器套接字主动为被动
tcp_socket.listen(128)
# 设置服务器套接字为监听状态,每监听到一个客户端发起的连接就创建一个专门服务
该客户端的新套接字,并返回该客户端的ip地址和端口信息
new_client_socket, client_addr = tcp_socket.accept()
print(client_addr)
# 新套接字接收客户端发送的信息
recv_data = new_client_socket.recv(1024)
print(recv_data)
# 新套接字收到信息后回送部分消息给客户端
new_client_socket.send("收到消息".encode("utf-8"))
# 关闭所有套接字
tcp_socket.close()
new_client_socket.close()
if __name__ == "__main__":
main()
·
实验结果:
·
tcp 通信模型
·
·
·
·
四. 总结:
- tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
- tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机
- tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
- 当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
- 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
- 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信
- 关闭accept返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线