1 需求分析
经分析,本程序是一个C/S结构,使用TCP协议实现聊天功能,需要实现的功能有如下几点。
- 本程序需要有客户端以及服务器端。
- 客户端应有良好的交互界面,服务器端应有转发客户端发来的消息和临时保存客户端发来的文件
- 本程序应支持多用户,用户可在线进行即时交流
- 用户端可发送文字,图片。且支持群聊和私聊
- 本程序登陆需要提供账号和密码,账号密码由数据库保存。
- 客户端提供注册功能,可选择设置自己的账号密码和昵称。
1.1客户端功能
- 客户端可在好友列表看到已上线的好友。
- 客户端可在输入框中编辑文字并且可以发送文字消息。
- 客户端可选择图片发送给指定好友
- 客户端可以进行群聊和私聊。
- 客户端保存服务器发来的图片文件,并显示。
- 客户端可显示服务器发过来的消息。
1.2服务器功能
- 接受文字消息并转发给指定客户端
- 接受并且保存客户端发来的图片并转发
- 每当有一个客户端连接,就给所有客户端更新好友列表
- 每当有一个客户端连接,就给所有客户端发送好友上线通知
- 每当有一个客户端离线,就给所有客户端发送好友下线通知
程序流程图
用例图
2.3.1用户用例图
用户用例图如下
2.3.2客户端用例图
2.3.3服务器端用例图
数据库设计
本程序使用mySQL数据库保存用户信息。
本程序所涉及到的表有用户表qq_user。
3.1用户表设计
如图为qq_user表的设计
数据库连接关键代码
private static String userName;
private static String password;
private static String id;
private static String connectString = "";
static {
try {
connectString = "jdbc:mysql://localhost:3306/qqpro"; //端口+数据库名字
password = "123456"; //mysql账号
userName = "root"; //mysql密码
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnect() {
try {
return DriverManager.getConnection(connectString, userName,password);
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
界面设计
4.1.1登录界面
图4-1登陆界面
4.1.2 注册界面
图4-2注册界面
4.1.3聊天主界面
图4-3主界面
5 功能展示
启动SERVER,然后启动三次Login,打开三个客户端。
点击登录进入主界面
接着PYTHON发送一条信息给所有人
再发一条图片给所有人
再由JAVA发送一条消息给C++。双击好友栏可私聊。
再发送图片给PYTHON
某一用户下线其他用户可收到系统提示消息,同时好友列表不再显示该用户,这里选择C++下线。
同样上线其他用户也能收到提示消息,这里C++再上线
再开一个客户端,进入注册界面,注册一个新的用户
点击注册按钮。
再使用该账号登录
功能展示完毕。
关键代码
Send类封装了客户端发送消息的功能
public class Send implements Runnable{
private DataOutputStream dos;
private FileInputStream fis=null;
byte[] sendBytes = new byte[1024];
private boolean isrunning = true;
private User user;
private OutputStream out;
private Socket client;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Send(){
}
public Send(Socket socket, User user){
this();
try {
this.user = user;
this.client = socket;
out = socket.getOutputStream();
dos = new DataOutputStream(out);
sendUserName();
} catch (IOException e) {
System.out.println("Send构造异常");
CloseUtil.closeAll(dos);
isrunning = false;
}
}
private void sendUserName(){
try {
if (user.getUser_name() != null &&! user.getUser_name().equals("")){
dos.writeUTF(user.getUser_name());
dos.flush();
}
} catch (IOException e) {
System.out.println("发送失败000");
isrunning = false;
CloseUtil.closeAll(dos);
}
}
private String getMsgFromUser(){
String info = null;
if (user.getMsg()!=null&& !user.getMsg().equals("")){
System.out.println("msg not null");
info = user.getMsg();
}
if (info!=null){
System.out.println(info);
}
return info;
}
public void send(String info){
try {
if (info != null &&!info.equals("")){
dos.writeUTF(info);
dos.flush();
user.setMsg(""); //清空待发送消息
}
} catch (IOException e) {
System.out.println("发送失败001");
isrunning = false;
CloseUtil.closeAll(dos);
}
}
public void sendImage(File file){
try {
int length = 0;
fis = new FileInputStream(file);
while ((length = fis.read(sendBytes, 0, sendBytes.length)) > 0) {
dos.write(sendBytes, 0, length);
dos.flush();
}
}catch(IOException e){
System.out.println(e.getMessage());
System.out.println("发送失败002");
isrunning = false;
CloseUtil.closeAll(dos, fis);
}
}
@Override
public void run() {
}
}
Receive类负责接受服务器端发来的消息
public class Receive implements Runnable {
//输入流
private DataInputStream dis;
private FileOutputStream fos=null;
private Socket socket;
byte[] inputByte = new byte[1024];
private boolean isrunning = true;
private UserView userView;
private InputStream ins;
private User user;
public Receive(){
}
public Receive(Socket socket, UserView userView){
this();
this.socket = socket;
this.userView = userView;
try {
//接受欢迎信息
ins = socket.getInputStream();
dis = new DataInputStream(socket.getInputStream());
String name = dis.readUTF();
user = new User();
user.setUser_name(name);
String str=dis.readUTF();
if (!str.equals("")){
UserView.FontAttrib fontAttrib = new UserView.FontAttrib(str);
userView.insert(fontAttrib);
}
} catch (IOException e) {
isrunning = false;
CloseUtil.closeAll(dis);
}
}
public void receive(){
String data = null;
try {
String tem = dis.readUTF();
System.out.println("接受命令"+tem);
if (!tem.equals("") && tem!=null){ //消息不为空
if (tem.equals(Server.CONSTANT_SERVER_MSG)){ //如果是系统消息
data = dis.readUTF(); //读数据
UserView.FontAttrib fontAttrib = new UserView.FontAttrib(data);
userView.insert(fontAttrib);
}else if (tem.equals(Server.CONSTANT_UPDATE_FRIEND_LIST)){ //更新好友列表消息
String str = dis.readUTF(); //读字符串
if (str!=null && !str.equals("")){
//更新好友列表
String[] split = str.split(",");
for (String s :split){
if (userView.getListModel1().contains(s)){
continue;
}else {
userView.getListModel1().addElement(s);
}
}
}
//好友下线消息
}else if (tem.equals(Server.CONSTANT_LOGIN_OUT)){
data = dis.readUTF();
UserView.FontAttrib fontAttrib = new UserView.FontAttrib(data);
userView.insert(fontAttrib);
data = dis.readUTF();
userView.getListModel1().removeElement(data);
//接受文字给所有人的消息
}else if (tem.equals(Server.CONSTANT_MSG_ALL)){
data = dis.readUTF();
UserView.FontAttrib fontAttrib = new UserView.FontAttrib(data);
userView.insert(fontAttrib);
//私聊消息
}else if (tem.equals(Server.CONSTANT_MSG)){
data = dis.readUTF();
UserView.FontAttrib fontAttrib = new UserView.FontAttrib(data);
userView.insert(fontAttrib);
//图片消息
}else if (tem.equals(Server.CONSTANT_IMAGE_ONE)||tem.equals(Server.CONSTANT_IMAGE_ALL)){
String fileString = dis.readUTF();
System.out.println("此处有filename"+fileString); //打印文件名+文件长度
String[] split = fileString.split("#");
ReceiveImage(tem, split[0],split[1]);
} else {
System.out.println("未被捕获的"+tem);
}
}
} catch (IOException e) {
System.out.println("错误");
isrunning = false;
CloseUtil.closeAll(dis);
}
}
private void ReceiveImage(String whoReceive, String fileName, String fileLength){
try {
System.out.println("创建用户本地文件夹");
File dir = new File(System.getProperty("user.dir")+"\\src\\Image\\User\\"+user.getUser_name());
if(!dir.exists()){
dir.mkdir(); //文件夹不存在,创建文件夹
}
File file = new File(dir, fileName);
fos = new FileOutputStream(file);
System.out.println("开始接收数据...");
int length = 0;
int Size_len =0;
int i = Integer.parseInt(fileLength); //约定读取长度等于文件长度时,跳出循环,防止阻塞!!!
while ((length = dis.read(inputByte, 0, inputByte.length)) > 0) {
System.out.println(length);
fos.write(inputByte, 0, length);
fos.flush();
Size_len = Size_len+length;
System.out.println(Size_len);
if (Size_len == i){
break;
}
}
System.out.println("完成接收");
//显示图片
userView.insertIcon(file, whoReceive);
} catch (IOException e) {
e.printStackTrace();
}
}
//线程体
@Override
public void run() {
while (isrunning){
receive();
}
}
}
服务器端代码
public class Server {
private List<MyChannel> all = new ArrayList<MyChannel>();
private List<String> allUser = new ArrayList<String>();
public static String CONSTANT_SERVER_MSG = "10000";
public static String CONSTANT_UPDATE_FRIEND_LIST = "10001";
public static String CONSTANT_MSG_ALL = "10002";
public static String CONSTANT_MSG = "10003";
public static String CONSTANT_LOGIN_OUT = "10004";
public static String CONSTANT_IMAGE_ONE = "10005";
public static String CONSTANT_IMAGE_ALL = "10006";
public static void main(String[] args) throws IOException {
//创建服务器+端口
new Server().start();
}
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
//接受客户端连接 阻塞式
while (true) {
Socket socket = serverSocket.accept();
System.out.println("一个客户端建立连接");
MyChannel Channel = new MyChannel(socket);
all.add(Channel);
new Thread(Channel).start();
}
}
private class MyChannel implements Runnable{
private DataOutputStream dos;
private DataInputStream dis;
private FileOutputStream fos = null;
private FileInputStream fis = null;
private byte[] inputByte = new byte[1024];
private byte[] sendBytes = new byte[1024];
private boolean isrunning=true;
private InputStream ins;
private OutputStream out;
private User user;
private Socket server;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public MyChannel(Socket socket) {
try {
server = socket;
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
ins = socket.getInputStream();
out = socket.getOutputStream();
user = new User();
user.setUser_name(dis.readUTF());
allUser.add(user.getUser_name());
this.send(user.getUser_name());
this.send(String.format("欢迎您(%s)进入群聊!【系统消息】", user.getUser_name()));
this.sendOthers(CONSTANT_SERVER_MSG);
this.sendOthers("您的好友("+user.getUser_name()+")已上线【系统消息】");
//发送已上线好友信息
StringBuilder sb = new StringBuilder();
if (allUser!=null && allUser.size()>0){
boolean flag = false;
for (String string : allUser) {
if (flag) {
sb.append(",");
} else {
flag = true;
}
sb.append(string);
}
System.out.println(sb.toString()); //01,02,03
}
this.send(CONSTANT_UPDATE_FRIEND_LIST); //发送更新标识
send(sb.toString());
this.sendOthers(CONSTANT_UPDATE_FRIEND_LIST); //发送更新标识
sendOthers(sb.toString());
} catch (IOException e) {
isrunning = false;
CloseUtil.closeAll(dos,dis);
}
}
private String receive(){
String msg = null;
try {
msg = dis.readUTF();
if (msg!=null && !msg.equals("")){
System.out.println("服务器收到"+user.getUser_name()+msg);
//表示即将收到的是图片文件
if (msg.startsWith("#image_content#")){
//保存文件
System.out.println("保存文件");
// BufferedInputStream bin = new BufferedInputStream(ins);
File dir = new File(System.getProperty("user.dir")+"\\src\\Image\\Server\\"+user.getUser_name());
if(!dir.exists()){
dir.mkdir(); //文件夹不存在,创建文件夹
}
//解析字符串
String [] strArr = msg.split("#");
String fileType = strArr[2]; //文件类型
String receiveUser = strArr[3]; //接收方
int fileLength = Integer.parseInt(strArr[4]); //文件长度
String imageName = new String(DateStringUtil.getTimeStr2() +"." + fileType); //保存文件名
File file = new File(dir, imageName);
// FileOutputStream fout = new FileOutputStream(file);
fos = new FileOutputStream(file);
System.out.println("开始接收数据...");
int length = 0;
int Size_len=0; //约定读取长度等于文件长度时,跳出循环,防止阻塞!!!
while ((length = dis.read(inputByte, 0, inputByte.length)) > 0) {
System.out.println(length);
fos.write(inputByte, 0, length);
fos.flush();
Size_len = Size_len+length;
System.out.println(Size_len);
if (Size_len == fileLength){
break;
}
}
// ins.close();
// ins.reset();
// ins = server.getInputStream();
// dis = new DataInputStream(ins);
// inputByte.notifyAll();
System.out.println("完成接收");
//发送文件
System.out.println("发送文件");
String s = String.valueOf(fileLength); //文件长度
sendImageToUser(file, receiveUser, imageName, s);
}else {
//私聊消息
if (msg.startsWith("@")){
String[] split = msg.split(":", 2);
String name = split[0].substring(1);
String message = split[1];
System.out.println(name);
sendOne(name, message); //发送
}else {
//群聊消息
sendOthers(CONSTANT_MSG_ALL);
sendOthers(user.getUser_name()+":"+msg);
send(CONSTANT_MSG_ALL);
send(user.getUser_name()+":"+msg);
}
}
}
} catch (IOException e) {
System.out.println("异常!" + user.getUser_name()+"已下线【系统消息】");
sendOthers(CONSTANT_LOGIN_OUT);
sendOthers("您的好友("+user.getUser_name()+")已下线【系统消息】");
sendOthers(user.getUser_name());
allUser.remove(user.getUser_name());
all.remove(this);
isrunning = false;
CloseUtil.closeAll(dis);
}
return msg;
}
private void sendImage(File file){
if (file!=null){
try {
fis = new FileInputStream(file);
int length =0;
while ((length = fis.read(sendBytes, 0, sendBytes.length)) > 0) {
dos.write(sendBytes, 0, length);
dos.flush();
}
// server.shutdownOutput();
// Thread.currentThread().sleep(2000);
// out = server.getOutputStream();
// dos = new DataOutputStream(out);
} catch (IOException e) {
System.out.println("发送image失败");
isrunning = false;
CloseUtil.closeAll(dos);
all.remove(this);
}
}
}
private void sendImageToUser(File file, String receiveUser, String imageName, String fileLength){
if (receiveUser.startsWith("@")){
String name = receiveUser.substring(1,receiveUser.length()-1);
sendOne(name," ");
for (MyChannel tem:all){
if (tem.getUser().getUser_name().equals(user.getUser_name())){
tem.send(CONSTANT_IMAGE_ONE);
tem.send(imageName+"#"+fileLength); //发送文件名+文件长度
tem.sendImage(file);
// tem.server.shutdownOutput(); //通知客户端文件已发送完
}
if (all.size() == 1){ //如果只有一个用户则认为是自己给自己发送图片,需要等待一段时间,避免接受失败!
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (tem.getUser().getUser_name().equals(name)){
tem.send(CONSTANT_IMAGE_ONE);
tem.send(imageName+"#"+fileLength); //发送文件名+文件长度
tem.sendImage(file);
// tem.server.shutdownOutput();
}
}
}else if (receiveUser.equals("all")){
System.out.println("image send to all");
for (MyChannel tem:all){
tem.send(CONSTANT_MSG_ALL);
tem.send(user.getUser_name()+":");
tem.send(CONSTANT_IMAGE_ALL);
tem.send(imageName+"#"+fileLength); //发送文件名+文件长度
//发送文件
tem.sendImage(file);
// tem.server.shutdownOutput();
}
}else {
System.out.println(receiveUser);
}
}
private void sendOne(String name, String msg){
for (MyChannel tem:all){
if (tem.getUser().getUser_name().equals(user.getUser_name())){
tem.send(CONSTANT_MSG);
tem.send("@我("+user.getUser_name()+")对("+name+")说:"+msg);
}
if (tem.getUser().getUser_name().equals(name)){
tem.send(CONSTANT_MSG);
tem.send("@("+user.getUser_name()+")对我("+name+")说:"+msg);
}
}
}
private void send(String msg){
if (msg!=null&&!msg.equals("")){
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
System.out.println("发送失败100");
isrunning = false;
CloseUtil.closeAll(dos);
all.remove(this);
}
}
}
private void sendOthers(String msg){
for (MyChannel other:all){
if (other == this){
continue;
}
other.send(msg);
}
}
@Override
public void run() {
while (isrunning){
receive();
}
}
}
}
以上就是该程序最重要的代码啦