sockert多线程聊天室
效果图
:
一、项目地址
二、socket简介
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
socket “插座”
socket 的典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
三、socket类型
根据数据的传输方式,可以将 Internet 套接字分成两种类型。通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。
1、流格式套接字(SOCK_STREAM)
流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用SOCK_STREAM 表示。
特点:
- 用TCP协议;
- 数据在传输过程中不会消失;
- 数据是按照顺序传输的;
- 数据的发送和接收不是同步的。
可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。
2、数据报格式套接字(SOCK_DGRAM)
数据报格式套接字(Datagram Sockets也叫“无连接的套接字”,代码中使用SOCK_DGRAM 表示。
特点
- 用UDP协议;
- 强调快速传输而非传输顺序;
- 传输的数据可能丢失也可能损毁;
- 限制每次传输的数据大小;
- 数据的发送和接收是同步的。
众所周知,速度是快递行业的生命。用摩托车发往同一地点的两件包裹无需保证顺序,只要以最快的速度交给客户就行。这种方式存在损坏或丢失的风险,而且包裹大小有一定限制。因此,想要传递大量包裹,就得分配发送。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。
四、socket通信逻辑
socket是基于C/S架构的,也就是说进行socket网络编程,通常需要编写两个文件,一个服务端,一个客户端。
其通信逻辑如下(图源网络):
五、socket创建
此处以Python语言为例,说明如何创建套接字。
在Python中,import socket后,用socket.socket()方法来创建套接字,语法格式如下:
sk = socket.socket([family[, type[, proto]]])
参数说明:
family
: 套接字家族,可以使AF_UNIX或者AF_INET。
type
: 套接字类型,根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM,也就是TCP和UDP的区别。
protocol
: 一般不填默认为0。
下面是具体的参数定义:
socket类型 | 描述 |
---|---|
socket.AF_UNIX | 只能够用于单一的Unix系统进程间通信 |
socket.AF_INET | IPv4 |
socket.AF_INET6 | IPv6 |
socket.SOCK_STREAM | 流式socket , for TCP |
socket.SOCK_DGRAM | 数据报式socket , for UDP |
socket.SOCK_RAW | 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。 |
socket.SOCK_SEQPACKET | 只能够用于单一的Unix系统进程间通信 |
创建TCP Socket: | s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) |
创建UDP Socket: | s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) |
通过sk = socket.socket()方法,我们可以获得一个socket对象sk,也就是通常说的获取了一个“套接字”,该对象具有以下方法:
服务器端方法 | 描述 |
---|---|
s.bind() | 绑定地址(host,port)到套接字,在AF_INET下,以元组(host,port)的形式表示地址 |
s.listen(backlog) | 开始监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。 |
s.accept() | 被动接受客户端连接,(阻塞式)等待连接的到来,并返回(conn,address)二元元组,其中conn是一个通信对象,可以用来接收和发送数据。address是连接客户端的地址。 |
客户端方法 | 描述 |
---|---|
s.connect(address) | 客户端向服务端发起连接。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 |
s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共方法 | 描述 |
---|---|
s.recv(bufsize) | 接收数据,数据以bytes类型返回,bufsize指定要接收的最大数据量。 |
s.sendall() | 完整发送数据。将数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。 |
s.recvform() | 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收的数据,address是发送数据的套接字地址。 |
s.close() | 关闭套接字,必须执行。 |
注意事项:
Python3以后,socket传递的都是bytes
类型的数据,字符串需要先转换一下,string.encode()即可;另一端接收到的bytes数据想转换成字符串,只要bytes.decode()一下就可以。
在正常通信时,accept()和recv()方法都是阻塞的。所谓的阻塞,指的是程序会暂停在那,一直等到有数据过来。
六、socket编程完整步骤(TCP)
1、服务端
-
创建套接字,绑定套接字到本地IP与端口:
socket.socket(socket.AF_INET,socket.SOCK_STREAM)
, -
设置socket选项(可选),s.bind() 开始监听连接:
s.listen()
-
进入循环,不断接受客户端的连接请求:s.accept() 接收传来的数据,或者发送数据给对方:
s.recv()
-
s.sendall() 传输完毕后,关闭套接字:
s.close()
2、客户端
- 创建套接字,连接服务器地址:
socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect()
- 连接后发送数据和接收数据:
s.sendall()
,s.recv()
- 传输完毕后,关闭套接字:
s.close()
七、socket(TCP)多线程图形化界面聊天室实例
1、需求分析
通过以上对socket编程的一些讲解,可以发现单纯的在命令行实现socket通信并不算太难,如果给它加上图形化界面,会更直观。所以我们就一起来实现一个socket(TCP)多线程图形化界面聊天室吧。
根据需求分析,此聊天室项目主要包含两个方面:
- socket网络编程
- 图形化界面
socket网络编程我们上面已经讲解过一些基础知识,接下来只要用Python去实现即可,。至于图形化界面,我们这里用的是python自带的GUI库tkinter,其跨平台性能良好,并且本身就是为快速GUI开发而生,已经比较成熟。除了tkinter,读者也可以使用其他的GUI框架,比如PyQt,此处就不具体实现了。
2、具体实现
我们结合代码具体讲解,此处主要包含两个py文件,一个服务器端server.py,一个客户端client.py。因为服务器端主要是接受客户端请求并返回消息,所以此处我们将图形化界面的编写融合到客户端的代码文件中,更加“直观”的感受客户端的请求发送。
需要注意的是,此处我们使用了Python内置的threading模块进行多线程处理,因为Python的socket模块,默认情况下创建的是单进程单线程,同时只能处理一个连接请求。
(1)服务器端
服务端主要包含用户字典库,以及服务端接受及处理服务端请求。
此处为类的定义(完整代码见:socket-chat-room):
import socket
import threading
from settings import Local_Host,Local_Port
#User database for authentication
db = {
'北落':'Love',
'师门':'You',
'北落师门':'Forever',
}
class ChatServer:
clients_list = [] #客户端列表
last_received_message = "" #接收的最新请求
def __init__(self):
self.server_socket = None
self.create_listening_ser
运行服务器端server.py,可以发现服务正在监听等待请求消息:
(2)客户端
客户端主要为图形化界面的实现,以及请求的发送与接收。
此处为类的定义(完整代码见:socket-chat-room)
from tkinter import Tk, Frame, Scrollbar, Label, END, Entry, Text, VERTICAL, Button, messagebox
import socket
import threading
class GUI:
client_socket = None
last_received_message = None
def __init__(self, master):
self.root = master
self.chat_transcript_area = None
self.name_widget = None
self.password_widget = None
self.enter_text_widget = None
self.join_button = None
self.initialize_socket() #初始化socket
self.initialize_gui() #图形化界面
self.listen_for_incoming_messages_in_a_thread() #监听消息
运行客户端代码client.py),可以看到服务端显示有客户端请求:
此时客户端部分,桌面会弹出一个新的页面,如下所示:
可以看到,图形化界面主要包含三个部分:
聊天区
:用于展示(多个)客户端发送的请求消息用户信息区
:用户的昵称、密码,点击加入群聊,服务端进行身份验证,通过后,可以开始聊天。消息编写发送区
:键入需要发送的消息,回车发送。
键入昵称与密码后,服务端会进行身份验证,主要是通过将昵称与密码结合成字符串,发送至服务端,服务端将其与自身保存的用户字典中的用户进行匹配。若在字典中,则验证通过,服务器端显示成功连接:
此时客户端的昵称、密码输入框以及加入群聊按钮都被禁用,即不允许再修改身份:
若身份验证失败,则服务端显示身份验证失败,断开连接:
此时客户端弹出身份验证失败提示框:
当新的客户端加入聊天室时,聊天区会展示该用户已加入群聊(has joined)。在两个客户端分别键入消息并发送,其结果如下:
注意:
-
通过tkinter的事件绑定,昵称、密码输入后,点击“加入群聊”按钮,输入框状态修改为"disabled",即无法再修改身份信息。
聊天区也绑定了事件,即聊天区消息无法通过键盘输入或修改。 -
聊天区无法修改历史聊天消息。
-
当未输入昵称或密码,直接点击“加入群聊按钮”,或者尝试在消息区编写发送消息,系统会弹出提示框,提醒用户先进行登录。
八、远程服务器测试
上面的聊天室实现是基于本机的,服务端与客户端绑定的都为本机ip:127.0.0.1,端口port:10319。那么如何进行远程服务器测试呢?
重要的一点是要将用于socket通信的远程服务器的端口打开,注意是出方向
和入方向
同时打开(笔者此处使用4004端口)。
服务器端口设置
并且需要将远程服务器ip绑定为"0.0.0.0",即允许监听所有端口。
设置后,客户端与服务端即可正常通信,亲测可用!
Reference
[1] https://www.liujiangblog.com/course/python/76
[2]http://c.biancheng.net/cpp/socket/