思路:

要实现聊天功能,我们就必须有服务器和客户端。客户端连接到服务器,然后通过发送消息到服务器及从服务器读取消息来达到多客户端通信的目的。简单来说,所有客户端都是通过服务器来进行身份验证和消息发送的。要达到通信的目的,我们首先要做的是实现多客户端与服务器的连接,当客户端连接上服务器之后,服务器需要做的就是每来一个客户端,就处理该客户端的业务,如登录,单聊等;客户端要做的就是通过读取服务器的数据、写入数据到服务器来实现与其他客户端的交互。

先通过两张图了解一下客户端与服务器的交互:

了解完大体思路之后,接下来我们来看一下各个步骤的具体实现方法:

  • 客户端与服务器的连接

套接字使用TCP可以实现两台计算机之间的通信,客户端创建一个套接字,并尝试连接服务器的套接字。java.net.Socket类代表一个套接字,java.net.ServerSocket类为服务器提供了一种监听并连接的通信机制。在连接过程中:

1. 服务器会创建一个ServerSocket对象,表示通过服务器的端口进行通信。

public ServerSocket(int port) throws IOExecption  //创建绑定到特定端口的服务器套接字

2. 服务器调用ServerSocket类的accept()方法,该方法将阻塞等待客户端的连接。

public Socket accept() throws IOException  //侦听并接收到此套接字的连接

3. 当服务器在等待客户端连接时,此时一个客户端创建一个Socket对象,并指定服务器名称和端口号请求连接。

public Socket(String host, int port) throws UnknownHostException, IOException. 创建一个
流套接字并将其连接到指定主机上的指定端口号。

4. 当客户端连接上服务器之后,在服务器端,accept()方法会返回一个新的socket引用,该socket连接到客户端的socket。

  • 服务器的工作

当客户端连接上服务器之后,我们知道accept()方法会返回给服务器一个新的socket引用,该socket连接到客户端的socket。那么此时只要有一个客户端连接到服务器了,服务器就要处理客户端的业务。我们知道每个客户端其实都是独立的,而且每个客户端连接服务器的时间都不定,所以我们可以创建一个线程池,每来一个客户端,就将该客户端的业务提交到线程池中,由线程池执行器来执行任务。由于每个客户的业务需求不同,我们可以创建一个类(ExecuteClient)专门来处理客户的业务。

  • 客户端的工作

当客户端连接上服务器之后,每一个客户端都会有一个Socket对象,该对象通过往服务器写入数据,从服务器读取数据可以实现不同客户端之间的通信。这时候我们也可以创建两个线程专门处理往服务器写入数据(WriteDataToServerThread)和从服务器读取数据(ReadDataFromServerThread)的功能。

那么服务器可以处理客户端的哪些业务呢?

由于客户端和服务器都有Socket对象,我们先来看看它们都能调用哪些方法:

public InputStream getInputStream() throws IOException  //返回此套接字的输入流
public OutputStream getOutputStream() throws IOException  //返回此套接字的输出流
public void close() throws IOException  //关闭此套接字

此前我们先做如下约定:

1. 当输入register:<userName>:<password>时,表示用户注册

2. 当输入login:<userName>:<password>时,表示用户登录

3.当输入private:<userName>:<message>时,表示单聊

4.当输入group:<message>时,表示群聊

5.当输入bye时,表示用户退出

  • 注册(register)

注册的时候有以下几点要求:

1. 用户名、密码均不能为空
2. 用户名、密码都仅支持字母和数字
3. 用户名长度:8-15,密码长度:6-10
4. 用户名不能重复,密码无要求
5. 注册信息存储(用户名+密码)

思路:当新用户注册时,首先会输入用户名和密码,在用户名和密码均满足上述要求时,我们需要对用户信息(用户名+密码)进行存储,以便再有新用户要注册时进行用户名是否重复的比较。在保存用户信息时,由于一个用户名对应一个密码,我们可以用Map<String,String>  USER_PASSWORD_MAP来保存,其中key为用户名,value为密码。当新用户注册成功后,告知该用户注册成功;否则将注册失败原因告知,如用户名重复,密码长度不对等。

//给客户端发送信息private void sendMessage(Socket client, String message) {try {OutputStream clientOutput = client.getOutputStream();OutputStreamWriter writer = new OutputStreamWriter(clientOutput);writer.write(message + "\n");writer.flush();} catch (IOException e) {e.printStackTrace();}}//注册private void register(String userName, String password, Socket client) {if (userName == null) {this.sendMessage(client, "用户名不能为空");return;}if (password == null) {this.sendMessage(client, "密码不能为空");return;}char[] name = userName.toCharArray();  //字符串转数组char[] passWord = password.toCharArray();  //字符串转数组int nLength = name.length;  //用户名的长度int pLength = passWord.length;  //密码的长度if (!((nLength >= 8 && nLength <= 15) && (pLength >= 6 && pLength <= 10))) {this.sendMessage(client, "输入的用户名或密码长度不符合要求(用户名长度8到15,密码长度6到10)");return;}for (char n : name) {if (!((n >= 'A' && n <= 'Z') || (n >= 'a' && n <= 'z') || (n >= '0' && n <= '9'))) {this.sendMessage(client, "用户名仅支持字母、数字");return;}}for (char p : passWord) {if (!((p >= 'A' && p <= 'Z') || (p >= 'a' && p <= 'z') || (p >= '0' && p <= '9'))) {this.sendMessage(client, "密码仅支持字母、数字");return;}}for (Map.Entry<String, String> entry : USER_PASSWORD_MAP.entrySet()) {if (userName.equals(entry.getKey())) {this.sendMessage(client, "用户名" + userName + "已被占用");return;}}USER_PASSWORD_MAP.put(userName, password);  //保存新注册的用户信息this.sendMessage(this.client, "用户" + userName + "注册成功");  //通知客户端注册成功}
  • 登录(login)

登录的时候有以下几点要求:

1. 用户名密码均正确
 2. 在线用户、离线用户存储
 3. 在线用户不能重复登录,离线用户可以再次登录

思路: 当用户注册并登录成功后,我们可以将其保存在在线用户中,以此来检测是否有在线用户重复登录;当用户离线后,将其保存在离线用户中。由于一个用户名对应一个用户,所以我们可以用Map<String,Socket> ONLINE_USER_MAP保存在线用户,Map<String,Socket> OFFLINE_USER_MAP保存离线用户。如果用户登录成功,则发送“用户登录成功”给该用户,否则发送未登录成功的原因,如用户名或密码不正确,在线用户不能重复登录等。

//给客户端发送信息private void sendMessage(Socket client, String message) {try {OutputStream clientOutput = client.getOutputStream();OutputStreamWriter writer = new OutputStreamWriter(clientOutput);writer.write(message + "\n");writer.flush();} catch (IOException e) {e.printStackTrace();}}//登录private void login(String userName, String password, Socket client) {for (Map.Entry<String, Socket> entry : ONLINE_USER_MAP.entrySet()) {if (userName.equals(entry.getKey())) {this.sendMessage(client, "用户" + userName + "已在线,不可重复登录");return;}}for (Map.Entry<String, String> entry : USER_PASSWORD_MAP.entrySet()) {if (userName.equals(entry.getKey()) && password.equals(entry.getValue())) {System.out.println("用户" + userName + "加入聊天室");ONLINE_USER_MAP.put(userName, client);  //将登录成功的用户存入 在线用户printOnlineUser(); //打印在线用户this.sendMessage(client, "用户" + userName + "登录成功");  //通知客户端登录成功return;}}this.sendMessage(client, "用户名或密码输入不正确");return;}
  • 单聊(privateChat)

单聊的时候有以下几点要求:

1. 不能自己给自己发消息
2. 不支持给离线用户发送消息

思路:我们知道单聊是一个用户(currentUserName)给另一个用户(target) 发消息(message),那么为了用户更好的体验,在发送信息时,我们应该将发送者的用户名告知给被发送者。如果被发送者是自己,则不发送信息;如果被发送者已离线,则告知发送者被发送者已离线。

//获取当前用户的用户名private String getCurrentUserName() {String currentUserName = null;for (Map.Entry<String, Socket> entry : ONLINE_USER_MAP.entrySet()) {if (entry.getValue().equals(this.client)) {currentUserName = entry.getKey();break;}}return currentUserName;}//给客户端发送信息private void sendMessage(Socket client, String message) {try {OutputStream clientOutput = client.getOutputStream();OutputStreamWriter writer = new OutputStreamWriter(clientOutput);writer.write(message + "\n");writer.flush();} catch (IOException e) {e.printStackTrace();}} //单聊private void privateChat(String userName, String message) {String currentUserName = this.getCurrentUserName();Socket target = ONLINE_USER_MAP.get(userName);if (currentUserName.equals(userName)) {  //不能自己给自己发消息return;}if (target != null) {this.sendMessage(target, currentUserName + "对你说:" + message);} else {this.sendMessage(this.client, "用户" + userName + "已下线,此条消息未发出");}}
  • 群聊(groupChat)

群聊的时候有以下几点要求:

1. 发送者发出的消息只显示给其他在线用户(不包括发送者本人)

思路:群聊即一个用户发了消息,其他在线用户都能收到消息并做出回应。

 //获取当前用户的用户名private String getCurrentUserName() {String currentUserName = null;for (Map.Entry<String, Socket> entry : ONLINE_USER_MAP.entrySet()) {if (entry.getValue().equals(this.client)) {currentUserName = entry.getKey();break;}}return currentUserName;}//给客户端发送信息private void sendMessage(Socket client, String message) {try {OutputStream clientOutput = client.getOutputStream();OutputStreamWriter writer = new OutputStreamWriter(clientOutput);writer.write(message + "\n");writer.flush();} catch (IOException e) {e.printStackTrace();}}//群聊private void groupChat(String message) {String currentUserName = this.getCurrentUserName();for (Socket socket : ONLINE_USER_MAP.values()) {if (socket.equals(this.client)) {continue;}this.sendMessage(socket, currentUserName + "说:" + message);}}
  • 退出(quit)

思路:因为客户端每输入一句话都会发给服务器,然后服务器根据用户输入的信息作出相应的处理。当用户输入bye时,表明用户要退出,此时用户将关闭客户端,且停止往服务器写入数据功能。当服务器收到用户发送的bye之后,服务器将该用户从在线用户中去除,之后发送bye给用户,通知用户可以退出了。

//获取当前用户的用户名private String getCurrentUserName() {String currentUserName = null;for (Map.Entry<String, Socket> entry : ONLINE_USER_MAP.entrySet()) {if (entry.getValue().equals(this.client)) {currentUserName = entry.getKey();break;}}return currentUserName;}//退出private void quit() {String currentUserName = this.getCurrentUserName();System.out.println("用户" + currentUserName + "下线");Socket socket = ONLINE_USER_MAP.get(currentUserName);this.sendMessage(socket, "bye");ONLINE_USER_MAP.remove(currentUserName);printOnlineUser();OFFLINE_USER_MAP.put(currentUserName, socket);printOfflineUser();}

完整源代码如下:

github地址:https://github.com/huiforeverlin/chat_room

服务器:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;//服务器主类
public class MultiThreadServer {public static void main(String[] args) {final ExecutorService executorService = Executors.newFixedThreadPool(10);//核心线程10个int port = 6666;//默认端口try {if (args.length > 0) {  //命令行参数大于0,则将第一个参数作为端口值port = Integer.parseInt(args[0]);}} catch (NumberFormatException e) {System.out.println("端口参数不正确,将采用默认端口:" + port);}try {ServerSocket serverSocket = new ServerSocket(port);  //servereSocket表示服务器System.out.println("等待客户端连接...");while (true) {Socket client = serverSocket.accept();  //支持多客户端连接System.out.println("客户端连接端口号:" + client.getPort());executorService.submit(new ExecuteClient(client));  //通过提交任务到线程池来执行每个客户的业务}} catch (IOException e) {e.printStackTrace();}}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentHashMap;//执行客户业务
public class ExecuteClient implements Runnable {private final Socket client; //客户private static final Map<String, Socket> ONLINE_USER_MAP = new ConcurrentHashMap<String, Socket>();//在线用户private static final Map<String, Socket> OFFLINE_USER_MAP = new ConcurrentHashMap<String, Socket>();//离线用户private static final Map<String, String> USER_PASSWORD_MAP = new ConcurrentHashMap<>(); //用户信息(用户名,密码)public ExecuteClient(Socket client) {this.client = client;}//实现run方法public void run() {try {InputStream clientInput = client.getInputStream();  //从客户端读取数据Scanner scanner = new Scanner(clientInput);while (true) {String line = scanner.nextLine();if (line.startsWith("register")) { //注册String[] segments = line.split("\\:");String userName = segments[1];  //第一个参数表示用户名String password = segments[2];  //第二个参数表示密码this.register(userName, password, client);continue;}if (line.startsWith("login")) { //登录String[] segments = line.split("\\:");String userName = segments[1];  //第一个参数表示用户名String password = segments[2];  //第二个参数表示密码this.login(userName, password, client);continue;}if (line.startsWith("private")) {  //单聊String[] segments = line.split("\\:");String userName = segments[1];  //第一个参数表示被发送者的名称String message = segments[2];   //第二个参数表示要发送的信息this.privateChat(userName, message);continue;}if (line.startsWith("group")) {  //群聊String message = line.split("\\:")[1]; //要发送的信息this.groupChat(message);continue;}if (line.startsWith("bye")) {  //退出this.quit();break;}}} catch (IOException e) {e.printStackTrace();}}//注册private void register(String userName, String password, Socket client) {if (userName == null) {this.sendMessage(client, "用户名不能为空");return;}if (password == null) {this.sendMessage(client, "密码不能为空");return;}char[] name = userName.toCharArray();  //字符串转数组char[] passWord = password.toCharArray();  //字符串转数组int nLength = name.length;  //用户名的长度int pLength = passWord.length;  //密码的长度if (!((nLength >= 8 && nLength <= 15) && (pLength >= 6 && pLength <= 10))) {this.sendMessage(client, "输入的用户名或密码长度不符合要求(用户名长度8到15,密码长度6到10)");return;}for (char n : name) {if (!((n >= 'A' && n <= 'Z') || (n >= 'a' && n <= 'z') || (n >= '0' && n <= '9'))) {this.sendMessage(client, "用户名仅支持字母、数字");return;}}for (char p : passWord) {if (!((p >= 'A' && p <= 'Z') || (p >= 'a' && p <= 'z') || (p >= '0' && p <= '9'))) {this.sendMessage(client, "密码仅支持字母、数字");return;}}for (Map.Entry<String, String> entry : USER_PASSWORD_MAP.entrySet()) {if (userName.equals(entry.getKey())) {this.sendMessage(client, "用户名" + userName + "已被占用");return;}}USER_PASSWORD_MAP.put(userName, password);  //保存新注册的用户信息this.sendMessage(this.client, "用户" + userName + "注册成功");  //通知客户端注册成功}//登录private void login(String userName, String password, Socket client) {for (Map.Entry<String, Socket> entry : ONLINE_USER_MAP.entrySet()) {if (userName.equals(entry.getKey())) {this.sendMessage(client, "用户" + userName + "已在线,不可重复登录");return;}}for (Map.Entry<String, String> entry : USER_PASSWORD_MAP.entrySet()) {if (userName.equals(entry.getKey()) && password.equals(entry.getValue())) {System.out.println("用户" + userName + "加入聊天室");ONLINE_USER_MAP.put(userName, client);  //将登录成功的用户存入 在线用户printOnlineUser(); //打印在线用户this.sendMessage(client, "用户" + userName + "登录成功");  //通知客户端登录成功return;}}this.sendMessage(client, "用户名或密码输入不正确");return;}//单聊private void privateChat(String userName, String message) {String currentUserName = this.getCurrentUserName();Socket target = ONLINE_USER_MAP.get(userName);if (currentUserName.equals(userName)) {  //不能自己给自己发消息return;}if (target != null) {this.sendMessage(target, currentUserName + "对你说:" + message);} else {this.sendMessage(this.client, "用户" + userName + "已下线,此条消息未发出");}}//群聊private void groupChat(String message) {String currentUserName = this.getCurrentUserName();for (Socket socket : ONLINE_USER_MAP.values()) {if (socket.equals(this.client)) {continue;}this.sendMessage(socket, currentUserName + "说:" + message);}}//退出private void quit() {String currentUserName = this.getCurrentUserName();System.out.println("用户" + currentUserName + "下线");Socket socket = ONLINE_USER_MAP.get(currentUserName);this.sendMessage(socket, "bye");ONLINE_USER_MAP.remove(currentUserName);printOnlineUser();OFFLINE_USER_MAP.put(currentUserName, socket);printOfflineUser();}//给客户端发送信息private void sendMessage(Socket client, String message) {try {OutputStream clientOutput = client.getOutputStream();OutputStreamWriter writer = new OutputStreamWriter(clientOutput);writer.write(message + "\n");writer.flush();} catch (IOException e) {e.printStackTrace();}}//获取当前用户的用户名private String getCurrentUserName() {String currentUserName = null;for (Map.Entry<String, Socket> entry : ONLINE_USER_MAP.entrySet()) {if (entry.getValue().equals(this.client)) {currentUserName = entry.getKey();break;}}return currentUserName;}//打印在线用户private void printOnlineUser() {System.out.println("在线用户人数:" + ONLINE_USER_MAP.size() + ", 用户列表:");for (Map.Entry<String, Socket> entry : ONLINE_USER_MAP.entrySet()) {System.out.print(entry.getKey() + " ");}System.out.println("");}//打印离线用户private void printOfflineUser() {System.out.println("离线用户人数:" + OFFLINE_USER_MAP.size() + ", 离线用户:");for (Map.Entry<String, Socket> entry : OFFLINE_USER_MAP.entrySet()) {System.out.print(entry.getKey() + " ");}System.out.println("");}
}

客户端:

import java.io.IOException;
import java.net.Socket;//客户端主类
public class MultiThreadClient {public static void main(String[] args) {try {String host = "127.0.0.1";int port = 6666;try {if (args.length > 0) {port = Integer.parseInt(args[0]);}} catch (NumberFormatException e) {System.out.println("端口参数不正确,将采用默认端口:" + port);}if (args.length > 1) {host = args[1];}final Socket client = new Socket(host, port);new WriteDataToServerThread(client).start();new ReadDataFromServerThread(client).start();} catch (IOException e) {e.printStackTrace();}}
}
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Scanner;//从服务器读取数据
public class ReadDataFromServerThread extends Thread {private final Socket client;public ReadDataFromServerThread(Socket client) {this.client = client;}@Overridepublic void run() {try {InputStream clientInput = client.getInputStream();Scanner scanner = new Scanner(clientInput);while (true) {String message = scanner.nextLine();System.out.println("来自服务器的消息:" + message);if (message.equals("bye")) {break;}}} catch (IOException e) {e.printStackTrace();}}
}
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;//往服务器写入数据
public class WriteDataToServerThread extends Thread {private final Socket client;public WriteDataToServerThread(Socket client) {this.client = client;}@Overridepublic void run() {try {OutputStream clientOutput = client.getOutputStream();OutputStreamWriter writer = new OutputStreamWriter(clientOutput);Scanner scanner = new Scanner(System.in);while (true) {System.out.println("请输入消息:");String message = scanner.nextLine();writer.write(message + "\n");writer.flush();if (message.equals("bye")) {client.close();  //客户端关闭退出break;}}} catch (IOException e) {e.printStackTrace();}}
}

基于Socket编程的聊天工具【Java实现】相关推荐

  1. 基于Socket的简易聊天工具

    文章目录 基于Socket的简易聊天工具 简易聊天工具的功能: 项目设计 1.界面设计 2.类的设计 程序编写 1.ChatServer 2.ChatFrame 窗体的设计 1.ChatFrame的窗 ...

  2. 【Android】基于Socket的即时聊天(群聊)

    近来感觉秋招无望,学习Socket的时候,便做了个基于Socket的群聊工具: 先看看最终效果吧 项目GitHub通道(详细代码请自行copy) 如何利用Socket通信 socket又称为" ...

  3. 基于UDP的P2P聊天工具——0.2

    基于UDP的P2P聊天工具 0.2 简介: 1)这也是一个windows的P2P聊天工具: 2)它修复了0.1版的一个bug: 3)它为0.3版做了一点准备: 相关内容: 1)如果对端端口未开启服务, ...

  4. 基于UDP的P2P聊天工具 0.3——消息队列和重传

    基于UDP的P2P聊天工具 0.3--消息队列和重传 简介: 1)这是一个Windows的P2P聊天工具: 2)相比0.2,它多了定时重传的机制: 3)对局域网来说有些鸡肋,就当是为跨局域网做准备吧: ...

  5. socket recv 服务端阻塞 python_网络编程(基于socket编程)

    网络编程(基于socket编程) socket套接字:应用程序通常通过socket"套接字"向网络发送请求或应答网络请求,是主机间或同一计算机中的进程间相互通讯 socket是介于 ...

  6. python socket 网络聊天室_Python基于Socket实现简单聊天室

    本文实例为大家分享了Python基于Socket实现简单聊天室,供大家参考,具体内容如下 服务端 #!/usr/bin/env python # -*- coding: utf-8 -*- # @Ti ...

  7. 基于WebServices简易网络聊天工具的设计与实现

    基于WebServices简易网络聊天工具的设计与实现 Copyright 朱向洋 Sunsea ALL Right Reserved 一.项目内容 本次课程实现一个类似QQ的网络聊天软件的功能:服务 ...

  8. C# Winform基于socket编程的五子棋游戏(带聊天和发送文件功能)

    最近在做课设,题目是关于socket编程的一对一网络小游戏.期间遇到各种问题,也从中学到了很多.在此记录下课设中遇到的问题. 题目要求: 设计4 网络版小游戏 1 设计目的 1)熟悉开发工具(Visu ...

  9. 基于TCP的QQ聊天工具

    ###前言: 基于JAVA语言开发的一款网络聊天工具,通过Socket实现TCP编程,使用多线程实现了多客户端的连接.模仿腾讯QQ的界面,功能较为简单,但是使用了最基本的网络编程技术,如socket. ...

最新文章

  1. iOS 10.3下使用Fiddler抓取HTTPS请求
  2. 采用Android的MediaPlayer+SurfaceView设计视频播放器
  3. UA MATH571B 试验设计 QE练习题 不使用代码分析试验结果I
  4. 第四届中国国际大数据大会务实推进应用落地
  5. Learning to Rank中Pointwise关于PRank算法源码实现
  6. 数据结构与算法 / 分治算法
  7. matlab判断文件是否损坏,检查 MATLAB 代码文件是否有问题
  8. 操作系统:Win10的沙盒是什么,如何使用,看完你就懂了!
  9. by group 累加中文字段_EF 求和 GroupBy多个字段
  10. 《延世大学韩国语教程2》第十九课 生病(下)
  11. 个人Androidstudio快捷键及常用设置配置
  12. 如何在IGV上使用BLAT搜索非模式物种
  13. 程序员等于吃青春饭吗?
  14. 公安联勤指挥调度实战应用系统软件平台解决方案
  15. 工业制造厂房vr虚拟实景展示,真实立体呈现到客户面前
  16. 前端播放rtmp协议的视频流文件
  17. PHP面向对象-多态
  18. 移动端软件测试面试题及答案-2021年最新版
  19. PDF(复制、黏贴)时出现乱码之处理方法之一
  20. matlab读.h5文件

热门文章

  1. A*算法中启发函数的使用
  2. 卡尔曼滤波通俗易懂的解释
  3. Chrome浏览器无法开启声音,并且音量合成器中没有选项
  4. 2023年湖北建筑安全员ABC报考,来考网
  5. TLS-SRTP协议详解
  6. jquer_shijian 增加初始化 年月日 及 结束时间 年月日
  7. MPEG2 TS与ISMA的比较
  8. java web网上书店_java web简易网上书店项目系列,使用MVC模式(servlet+jstl+dbutils),开篇...
  9. python爬取机票信息
  10. 如何在没公网IP的情况下把电脑当成服务器来做一个简陋的html网站-Windows IIS篇