d0304 更新功能实现
d0312 更新部分图片&UI设计部分
d0318 更新功能实现
d1222 实现添加好友功能、实现注册功能、修改大量BUG
github:https://github.com/He11oLiu/ChatRoom.git
==========================================================
即时通讯(Instant messaging,简称IM)是一个终端服务,允许两人或多人使用网路即时的传递文字讯息、档案、语音与视频交流。如QQ,微信都属于即时通讯。
本篇博客将详细记录我在JAVA上搭建一个自己的即时通讯工具的实现方法,如有错误的地方或者建议,请多多提出。
功能实现
基本技术
- Socket 与 ServerSocket
作为一个即时通讯工具,客户端(Client)和服务器(Server)是两个必不可少的部分。首先我们就来解决服务器和客户端连接的问题。
Socket的英文原义是“孔”或“插座”。
Socket用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。
在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。
- 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。建立了一个Socket后,从这个Socket中读取I/O流,就可以实现两个程序的通讯。
而服务器需要与多个客户端进行双向的通讯连接,并非单一的连接。于是可以通过每接收到一个Client后就开一个单独的进程,来完成与该Client的通讯步骤。这样就可以同时接受多个Client,从而实现服务器的功能。 - 客户端建立:
Java提供了Socket类,用来建立客户端
提供的构造方法如下: - 当调用构造方法,构造一个新的Socket的时候,就是向指定的端口请求连接。
通过以下方法,可以获取该Socket的I/O流 - 通过close方法,关闭该Socket
- 服务器建立:
Java提供了ServerSocket类,用来建立服务器。
提供的构造方法如下:
可以通过Telnet来测试是否成功的建立了服务器。(win7后默认关闭,在控制面板-程序-启用或关闭windows功能下开启)
telnet localhost <port>
或者
telnet 127.0.0.1 <port>
该类下自带了
accept方法用来接收连接,返回一个Socket对象。接收该对象,并调用刚才提到的getInputStream()方法以及getOutputStream()方法,即可实现获取该Socket的I/O流。从该流上读取信息或者写入信息,即可达到通讯的目的。要注意的是,此时客户端的Input流对应着服务器端的Output流。
同样ServerSocket也带了close方法,用来关闭服务器。
- 通讯协议设计
通讯协议具体内容
type | 值 | 描述 | 类 |
1.注册请求信息 | 0x01 | 客户发送注册请求数据给服务器 | MsgReg |
2.注册应答消息 | 0x11 | 客户端返回注册结果 | MsgRegResp |
3.登陆请求信息 | 0x02 | 客户端发送包括JK号及密码 | MsgLogin |
4.登录应答消息 | 0x22 | 服务器返回登录结果 | MsgLoginResp |
5.添加好友信息 | 0x05 | 客户端向服务器发送添加好友JK号和添加好友所在列表 | MsgAddFriend |
6.添加好友应答信息 | 0x55 | 服务器返回添加好友结果 | MsgAddFriendResp |
7.好友列表 | 0x03 | 服务器给用户发送好友列表信息 | MsgTeamList |
8.聊天信息 | 0x04 | 客户端给服务器/服务器给客户端消息 | MsgChatText |
消息头(MsgHead)
MsgHead | 消息头(13) | 消息体 |
int totalLen | 消息总长度(4) | |
byte type | 消息类型(1) | |
int desk | 目标JK号(4) | 服务器的JK号:2000000000 |
int src | 发送用户JK号(4) | 登录发送JK号:2000000001 |
注册消息体(MsgReg extends MsgHead)
成员属性 | 类型 | 长度(总长33) | |
String nikeName | Octet String | 10 | 昵称 |
String Pwd | Octet String | 10 | 密码 |
注册应答消息(MsgRegResp)
成员属性 | 类型 | 长度(总长14) | |
byte state | byte | 1 | 若state为0.desk为目标JK号;若state为1(或其它)错误 |
登录请求信息(MsgLogin)
成员属性 | 类型 | 长度(总长23) | |
String pwd | Octet String | 10 | 密码JK号保存到src中 |
登录应答信息(MsgLoginResp)
成员属性 | 类型 | 长度(14) | |
byte state | byte | 1 | 若为0:登录成功;若为1:JK/pwd错误;若为2:ip错误;其它:未知错误 |
添加好友信息 (MsgAddFriend)
成员属性 | 类型 | 长度(27) | |
int add_ID | int | 4 | 所添加好友的JK号 |
String list_name | Octet String | 10 | 添加好友至好友列表的名称 |
添加好友应答信息 (MsgAddFriendResp)
成员属性 | 类型 | 长度(14) | |
byte state | byte | 1 | 若为0:添加好友成功;若为1:不存在该用户;若为2:已经存在该好友;若为3:创建好友列表失败 |
好友列表信息(MsgTeamList)
成员属性 | 类型 | 长度(总长:利用计算长度) | |
String UserName | Octet String | 10 | 用户名字 |
int UserPic | int | 4 | 用户头像 |
byte listCount | byte | 1 | 好友分组个数,表示有几组 |
String listName | Octet String | 10 | 分组名称 |
byte bodyCount | byte | 1 | 本组有多少个用户 |
int bodyPic | int | 4 | 好友头像 |
int bodyNum | int | 4 | 好友的JK号 |
String nikeName | Octet String | 10 | 好友 |
byte bodyState | byte | 1 | 好友状态,0:在线 |
聊天信息(MsgChatText)
成员属性 | 类型 | 长度 | |
String msgText | Octet String | 13 | 可聊天的内容 |
发送一次信息的总流程
框架设计
界面层
ChatRoom的主要界面有:
- 登陆界面
- 注册界面
- 好友列表界面
- 添加好友界面
- 聊天界面
业务逻辑层
- 服务器业务逻辑层
服务器主要提供以下功能
服务器主线程:
功能 | 创建服务器 |
描述 | 创建服务器。 |
动作 | 根据指定的port创建服务器。 |
输入 | port(端口) |
服务器对应每一个连接的单独线程ServerThread
这些ServerThread利用HashMap保存到线程库,key为JK号
功能 | 接受Client |
描述 | 接受客户端,创建单独线程,并通过输入输出流通信。 |
动作 | 循环监听是否有client接入,若有,则创建单独线程对其进行操作。 |
相关 | 服务器服务线程,ServerThread |
功能 | 响应登陆请求&发送好友列表 |
描述 | 响应客户端发送的登陆请求。 |
输入 | 根据输入流中读取的数据以及通讯协议,读取userid和pwd。 |
行动 | 利用数据库查询输入的userid和pwd是否匹配 |
输出 | 根据通讯协议,向输出流输出回执信息,包含最终匹配结果。 |
行动 | 若登陆信息匹配,更新用户在线表。 |
输出 | 若登陆信息匹配,则再根据通讯协议,向输出流发送好友列表。 |
相关 | 数据库,通讯协议 |
功能 | 一对一聊天 |
描述 | 响应客户端的聊天请求,并将聊天信息发到指定客户端。 |
输入 | 根据输入流中读取的数据以及通讯协议,获取目标userid以及发送内容。 |
行动 | 根据目标userid以及发送内容,给对应客户端发送信息。 |
输出 | 通过寻找对应客户端所接入的thread,给对应客户端发送内容。 |
相关 | 通讯协议,serverthread |
功能 | 注册用户 |
描述 | 响应客户端的注册请求,并利用数据访问层提供接口写入数据库 |
输入 | 根据输入流中读取的数据以及通讯协议,获取目注册用户名以及登陆密码 |
行动 | 利用数据访问层提供接口写入数据库 |
输出 | 若注册成功,返回客户端住车好的JK号 |
相关 | 通讯协议,serverthread |
功能 | 添加好友 |
描述 | 响应客户端的添加好友,利用数据访问层提供接口。 |
输入 | 客户端发送的添加好友的JK号已经好友列表名称 |
行动 | 利用数据访问层提供的接口判断输入的正确性,若正确在数据库中好友列表添加内容 |
输出 | 添加好友的状态。 |
相关 | 通讯协议,serverthread |
- 客户端业务逻辑层
功能 | 连接服务器 |
描述 | 客户端连接服务器,确认服务器可以连接。 |
输入 | 读取类中的ServerIP和port。 |
动作 | 根据ServerIP和port,开启Socket,获取输入输出流。 |
输出 | 能否连接到服务器 |
功能 | 注册 |
描述 | 输入用户名和密码 |
输入 | NikeName和Password |
动作 | 将注册请求,打包成消息,发送给服务器。 |
输出 | 根据服务器返回结果显示JK号或者显示注册失败。 |
功能 | 登陆服务器 |
描述 | 客户端申请登陆,确认用户密码是否正确。 |
输入 | 用户输入的userid和password |
动作 | 将userid和password根据通讯协议传输到服务器,并接收服务器回信。 |
输出 | 用户名密码是否正确 |
相关 | 通讯协议 |
功能 | 获取好友列表 |
描述 | 完成登陆后,从服务器获取好友列表,并显示出好友界面 |
动作 | 从服务器获取好友列表,并通过给好友列表界面对象。 |
相关 | 通讯协议,好友列表界面对象 |
功能 | 一对一聊天 |
描述 | 在好友列表中点击一个好友,开始一对一聊天,可以发送/接收信息 |
动作 | 根据通讯协议,向指定用户发送信息 |
输入 | 发送的目标userid以及发送内容(聊天界面传入) |
动作 | 根据通讯协议,读取信息来源用户,通过聊天界面显示内容。 |
输出 | 聊天内容传给聊天界面对象 |
相关 | 通讯协议,聊天界面对象 |
功能 | 添加好友 |
描述 | 添加好友到列表 |
输入 | 好友的JK号和列表名 |
动作 | 将请求打包成消息,发送给服务器,等待服务器返回结果 |
输出 | 根据结果弹出结果消息框,更新好友列表。 |
数据访问层
本部分由黄成越同学合作完成(https://hcyue.me/)
本次数据库选用SQLite数据库,数据库结构如下。
数据访问层提供UserModel,其中包括
- 根据JK号获取用户内容
- 验证用户信息
- 获取用户好友列表
- 增加好友,删除好友
UI设计
- Metro UI
Metro UI是基于瑞士平面设计原则,其最初在Windows XP的Windows Media Center就中有体现,这有利于以文字为主的界面导航。2006年著名的Zune播放器开始使用类似Metro的设计风格。微软的设计师计划重新设计现有用户界面、更清爽的排版和较少的重点以便于用户使用。Zune的桌面客户端程序也使用了不同于以往Portable Media Center用户界面,其清爽排版和设计给用户耳目一新的冲击。
Metro UI的特点:强调信息本身
参考资料:
- 窗体设置无边框以及拖动选项
为了彻底改变Swing的风格,实现Metro UI的界面,将原有的边框以及按钮重写是必不可少的。
首先,我们将窗体的边框取消
setUndecorated(true); //设置无边框
这行代码的作用,就是去掉了整个窗体的边框,但是同时也就去掉关闭按钮,缩小放大按钮。同时无法移动边框。为了能够移动边框,我们在窗体上添加一个监听器,代码如下。
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
isDraging = true;
wx = e.getX();
wy = e.getY();
}
public void mouseReleased(MouseEvent e) {
isDraging = false;
}
}); //确定鼠标按下的位置
addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
if (isDraging) {
int left = getLocation().x;
int top = getLocation().y;
setLocation(left + e.getX() - wx, top + e.getY() - wy);
}
}//更改窗体的位置
});
- 关闭、缩小、选择按钮的重写
因为选择了无边框,则原来的关闭以及缩小按钮都已经不能使用了,于是可以继承JButton,自己重写一个关闭,缩小按钮。同时为了配合Metro UI,选择按钮也需要重写注意实现鼠标移进、移出、按下的颜色改变(可以添加其他效果)。
各个组件实现代码见GitHub。 - 好友列表的实现
要实现好友列表,第一个想到的就是树状结构。但是要实现相对漂亮的UI,利用树状结果不太好实现。
经过多次尝试,最终确定用JScrollPane来完成这个可能需要拖动的好友列表。但是利用JScrollPane后,不宜布局,若利用JPanel来制作好友,其大小不好固定。最终重写滚动条,具体实现见代码。