目录
一、基础知识
1、TCP和UDP
TCP协议会通过握手建立连接,然后对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
许多常用的更高级的协议都是建立在TCP协议基础上的,例如用于浏览器的HTTP协议、发送邮件的SMTP协议等。
UDP协议同样建立在IP协议之上,但是UDP协议面向无连接的通信协议,不保证数据包的顺利到达,是不可靠传输,所以效率比TCP要高。
2、Socket
Socket是网络编程的一个抽象概念。Socket是套接字的英文名称,主要是用于网络通信编程。在20世纪80年代初,美国政府的高级研究工程机构(ARPA)给加利福尼亚大学的Berkeley分校提供了资金,让他们在UNIX操作系统下实现TCP/IP协议。在这个项目中,研究人员为TCP/IP网络通信开发了一个API(应用程序接口),这个API称为Socket(套接字)。Socket是TCP/IP网络最为通用的API,任何网络通信都是通过Socket来完成的。
通常用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型。
套接字构造函数socket(family,type[,protocol])使用给定的套接字家族、套接字类型、协议编号来创建套接字。
⭐ 其参数如下。
● family:套接字家族,可以是AF_UNIX或者AF_INET、AF_INET6。
● type:套接字类型,可以根据是面向连接的还是非连接的分为SOCK_STREAM和SOCK_DGRAM。
● protocol:一般不填,默认为0。
相关参数的取值意义如下:
创建TCP Socket:
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
创建UDP Socket:
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
Socket同时支持数据流Socket和数据报Socket,下图为面向连接支持数据流TCP的时序图。
由该图可以看出,客户机(Client)与服务器(Server)的关系是不对称的。
对于TCP/S,服务器首先后动,然后在某一时刻后动客户机与服务器建立连接。服务器与客户机开始都必须调用Socket()建立一个套接字,然后服务器调用Bind()将套接字与一个本机指定端口绑定在一起,再调用Listen()使套接字处于一种被动的准备接收状态,这时客户机建立套接字便可以通过调用Connect()和服务器建立连接,服务器就可以调用Accept()来接收客户机连接。然后继续监听指定端口,并发出阻塞,直到下一个请求出现,从而实现多个客户机连接。在连接建立之后,客户机和服务器之间就可以通过连接发送和接收数据。最后,待数据传送结束,双方调用Close()关闭套接字。
在Python的Socket模块中Socket对象提供的函数如下图所示:
3、编写一个简单的TCP服务器程序:
服务器端接收客户端连接,把客户端发过来的字符串加上“Hello”再发回去:
import socket
import threading
def tcplink(sock, addr):
try:
print('接收一个来自%s:%s 的连接请求' % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
except Exception as e:
print(f"发生错误: {e}")
finally:
sock.close()
print('来自%s:%s 的连接关闭了.' % addr)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8888))
s.listen(5)
print('等待客户端连接...')
while True:
try:
sock, addr = s.accept()
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()
except Exception as e:
print(f"接受连接时发生错误: {e}")
在上述代码中需注意:
1、在程序中首先创建一个基于IPv4和TCP协议的Socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
然后绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0绑定到所有的网络地址,还可以用127.0.0.1绑定到本机地址。
2、127.0.0.1是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。 端口号需要预先指定。因为这里写的这个服务不是标准服务,所以用8888这个端口号。
(注意:小于1024的端口号必须要有管理员权限才能绑定。)
# 监听本机8888端口
s.bind((127.0.0.1, 8888))
3、 接着调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量为5:
s.listen(5)
print("等待客户端连接..")
4、 接下来,服务器程序通过一个无限循环接受来自客户端的连接,accept()
会等待并返回一个客户端的连接:
while True:
# 接受一个新连接
sock, addr = s.accept() # sock是新建的Socket对象,服务器通过它与对应客户端通信,addr是IP地址
# 创建新线程来处理TCP连接
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()
5、每个连接都必须创建新线程(或进程)来处理,否则单线程在处理连接的过程中无法接受其他客户端的连接:
def tcplink(sock, addr):
print("接收一个来自%s:%s的连接请求" % addr)
sock.send(b'Welcome!')
# 发给客户端“Welcome!”信息
while True:
data = sock.recv(1024)
# 接收客户端发来的信息
time.sleep(1)
# 延时1秒钟
if not data or data.decode('utf-8') == 'exit':
# 如果没数据或收到'exit'信息
break
# 终止循环
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
# 收到信息加上“Hello”发回
sock.close()
# 关闭连接
print("来自%s:%s的连接关闭了。" % addr)
6、在连接建立后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上“Hello”再发送给客户端。如果客户端发送了exit字符串,就直接关闭连接。 如果要测试这个服务器程序,还需要编写一个客户端程序:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8888))
print(s.recv(1024).decode('utf-8'))
for data in [b'LiHua', b'XiaoMing', b'Jack']:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
print(s.recv(1024).decode('utf-8'))
s.close()
7、运行效果如下:
服务器端:
客户端:
8、需要注意的是:客户端程序运行完毕就退出了,而服务器程序会永远运行下去,必须按Ctrl+C组合键退出程序。
可见,用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后对每一个新的连接创建一个线程或进程来处理。
通常,服务器程序会无限运行下去。另外还需注意,同一个端口被一个Socket绑定了以后就不能被其他Socket绑定了。
二、多线程编程
1、线程:
线程是操作系统可以调度的最小执行单位,能够执行并发处理。通常是将程序拆分成两个或多个并发运行的线程,即同时执行多个操作。例如,在使用线程的同时监视用户并发输入,并执行后台任务等。
threading模块提供了Thread类来创建和处理线程,格式如下:
线程对象=threading.Thread(target=线程函数,args=(参数列表),name=线程名,group=线程组)
第一个参数是函数名,第二个参数args是一个元组,线程名和线程组都可以省略。
⭐ Thread 类还提供了以下方法:
- run(): 用于表示线程活动的方法。
- start(): 启动线程活动。
- join([time]): 可以阻塞进程,直到线程执行完毕。参数time指定超时时间(单位为秒),超过指定时间join就不再阻塞进程了。
- isAlive(): 返回线程是否活动。
- getName(): 返回线程名。
- setName(): 设置线程名。
⭐ threading 模块提供的其他方法如下:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同结果。
2、编写自己的线程类myTread来创建线程对象:
import threading
import time
exitFlag=0
class myThread(threading.Thread): # 继承父类threading.Thread
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self): # 把要执行的代码写到run()函数里面,线程在创建后会直接运行run()函数
print("starting " + self.name)
print_time(self.name, self.counter, 5)
print("Exiting " + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
threading.exit()
time.sleep(delay)
print("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启线程
thread1.start()
thread2.start()
print("Exiting Main Thread")
运行结果:
三、实现代码
客户端:
# 在线聊天程序客户端
import tkinter
import tkinter.font as tkFont
import socket
import threading
import time
import sys
class ClientUI():
local = '127.0.0.1'
port = 5505
global clientSock
flag=False
def __init__(self):
self.root = tkinter.Tk()
self.root.title('Python在线聊天-客户端V1.0')
# 窗口面板,用4个面板布局
self.frame = [tkinter.Frame(), tkinter.Frame(), tkinter.Frame(), tkinter.Frame()]
# 显示消息Text右边的滚动条
self.chatTextScrollBar = tkinter.Scrollbar(self.frame[0])
self.chatTextScrollBar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
# 显示消息Text,并绑定上面的滚动条
ft = tkFont.Font(family='Fixdsys', size=11)
self.chatText = tkinter.Listbox(self.frame[0], width=70, height=18, font=ft)
self.chatText['yscrollcommand'] = self.chatTextScrollBar.set
self.chatText.pack(expand=1, fill=tkinter.BOTH)
self.chatTextScrollBar['command'] = self.chatText.yview()
self.frame[0].pack(expand=1, fill=tkinter.BOTH)
# 标签,分开消息显示Text和消息输入Text
label = tkinter.Label(self.frame[1], height=2)
label.pack(fill=tkinter.BOTH)
self.frame[1].pack(expand=1, fill=tkinter.BOTH)
# 输入消息Text的滚动条
self.inputTextScrollBar = tkinter.Scrollbar(self.frame[2])
self.inputTextScrollBar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
# 输入消息Text,并与滚动条绑定
ft = tkFont.Font(family='Fixdsys', size=11)
self.inputText = tkinter.Text(self.frame[2], width=70, height=8, font=ft)
self.inputText['yscrollcommand'] = self.inputTextScrollBar.set
self.inputText.pack(expand=1, fill=tkinter.BOTH)
self.inputTextScrollBar['command'] = self.chatText.yview()
self.frame[2].pack(expand=1, fill=tkinter.BOTH)
# “发送”按钮
self.sendButton = tkinter.Button(self.frame[3], text='发送', width=10, command=self.sendMessage)
self.sendButton.pack(expand=1, side=tkinter.BOTTOM and tkinter.RIGHT, padx=15, pady=8)
# “关闭”按钮
self.closeButton = tkinter.Button(self.frame[3], text='关闭', width=10, command=self.close)
self.closeButton.pack(expand=1, side=tkinter.RIGHT, padx=15, pady=8)
self.frame[3].pack(expand=1, fill=tkinter.BOTH)
def receiveMessage(self):
try:
# 建立Socket连接
self.clientSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.clientSock.connect((self.local, self.port))
self.flag = True
except:
self.flag = False
self.chatText.insert(tkinter.END, '您还未与服务器端建立连接,请检查服务器是否启动')
return
self.buffer = 1024
self.clientSock.send('Y'.encode()) # 向服务器发送字符Y,表示客户端要连接服务器
while True:
try:
if self.flag == True:
# 连接建立,接收服务器端消息
self.serverMsg = self.clientSock.recv(self.buffer).decode('utf-8')
if self.serverMsg == 'Y':
self.chatText.insert(tkinter.END, '客户端已经与服务器端建立连接......')
elif self.serverMsg == 'N':
self.chatText.insert(tkinter.END, '客户端与服务器端建立连接失败......')
elif not self.serverMsg:
continue
else:
theTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.chatText.insert(tkinter.END, '服务器端' + theTime + '说:\n')
self.chatText.insert(tkinter.END, '' + self.serverMsg)
else:
break
except EOFError as msg:
raise msg
self.clientSock.close()
break
def sendMessage(self):
message = self.inputText.get('1.0', tkinter.END)
# 格式化当前的时间
theTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.chatText.insert(tkinter.END, '客户端' + theTime + '说:\n')
self.chatText.insert(tkinter.END, ' | ' + message + '\n')
if self.flag == True:
self.clientSock.send(message.encode()) # 将消息发送到服务器端
else:
# Socket连接没有建立,提示用户
self.chatText.insert(tkinter.END, '您还未与服务器端建立连接,服务器端无法收到您的消息\n')
# 清空用户在Text中输入的消息
self.inputText.delete(0.0, tkinter.END)
def close(self):
sys.exit()
def startNewThread(self):
# 启动一个新线程来接收服务器端的消息
thread = threading.Thread(target=self.receiveMessage, args=())
thread.setDaemon(True)
thread.start()
def main():
client = ClientUI()
client.startNewThread() # 启动线程接收服务器端的消息
client.root.mainloop()
if __name__ == '__main__':
main()
服务器端:
# 在线聊天程序服务器端
import tkinter
import tkinter.font as tkFont
import socket
import threading
import time
import sys
class ServerUI():
local = '127.0.0.1'
port = 5505
global serverSock
flag=False
def __init__(self):
self.root = tkinter.Tk()
self.root.title('Python在线聊天-服务器端V1.0')
self.frame = [tkinter.Frame() for _ in range(4)]
self.chatTextScrollBar = tkinter.Scrollbar(self.frame[0])
self.chatTextScrollBar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
ft = tkFont.Font(family='Fixdsys', size=11)
self.chatText = tkinter.Listbox(self.frame[0], width=70, height=18, font=ft)
self.chatText['yscrollcommand'] = self.chatTextScrollBar.set
self.chatText.pack(expand=1, fill=tkinter.BOTH)
self.chatTextScrollBar['command'] = self.chatText.yview()
self.frame[0].pack(expand=1, fill=tkinter.BOTH)
label = tkinter.Label(self.frame[1], height=2)
label.pack(fill=tkinter.BOTH)
self.frame[1].pack(expand=1, fill=tkinter.BOTH)
self.inputTextScrollBar = tkinter.Scrollbar(self.frame[2])
self.inputTextScrollBar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
self.inputText = tkinter.Text(self.frame[2], width=70, height=8, font=ft)
self.inputText['yscrollcommand'] = self.inputTextScrollBar.set
self.inputText.pack(expand=1, fill=tkinter.BOTH)
self.inputTextScrollBar['command'] = self.chatText.yview()
self.frame[2].pack(expand=1, fill=tkinter.BOTH)
self.sendButton = tkinter.Button(self.frame[3], text='发送', width=10, command=self.sendMessage)
self.sendButton.pack(expand=1, side=tkinter.BOTTOM, padx=25, pady=5)
self.closeButton = tkinter.Button(self.frame[3], text='关闭', width=10, command=self.close)
self.closeButton.pack(expand=1, side=tkinter.RIGHT, padx=25, pady=5)
self.frame[3].pack(expand=1, fill=tkinter.BOTH)
self.serverSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.serverSock.bind((self.local, self.port))
self.serverSock.listen(15)
self.buffer = 1024
self.chatText.insert(tkinter.END, '服务器已准备就绪......')
self.flag = False
def receiveMessage(self):
while True:
self.connection, self.address = self.serverSock.accept()
self.flag = True
while True:
clientMsg = self.connection.recv(self.buffer).decode('utf-8')
if not clientMsg:
continue
elif clientMsg == 'Y':
self.chatText.insert(tkinter.END, '服务器端已经与客户端建立连接......')
self.connection.send(b'Y')
elif clientMsg == 'N':
self.chatText.insert(tkinter.END, '服务器端与客户端建立连接失败......')
self.connection.send(b'N')
else:
theTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.chatText.insert(tkinter.END, '客户端{}说:\n{}'.format(theTime, clientMsg))
def sendMessage(self):
message = self.inputText.get('1.0', tkinter.END)
theTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.chatText.insert(tkinter.END, '服务器{}说:\n{}'.format(theTime, message))
if self.flag:
self.connection.send(message.encode())
else:
self.chatText.insert(tkinter.END, '您还未与客户端建立连接,客户端无法接收消息\n')
self.inputText.delete('1.0', tkinter.END)
def close(self):
sys.exit()
def startNewThread(self):
thread = threading.Thread(target=self.receiveMessage, args=())
thread.setDaemon(True)
thread.start()
def main():
server = ServerUI()
server.startNewThread()
server.root.mainloop()
if __name__ == '__main__':
main()
四、最终效果演示