前两天想到一个手机APP项目,使用到蓝牙,发现BluetoothSocket和J2EE网络变成的Socket差不多,使用之余顺手写一个多线程服务器与客户端交互实现聊天室的一个小例子,方便新人学习网络编程模块,期间使用到多线程和IO输入输出流的操作,有点儿不明白的过后我会有一些个人使用心得总结,敬请期待哈!
源码内容十分简单,我工程文件我存在下面的地址上去了,方便大家下载,0积分,为了方便大家学习了。
下载地址请移步
Server.Java文件,主要是服务端管理,通过多线程接收各用户发送消息以及消息转送等处理。
package cn.com.dnyy.tcp;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Server {
// 用以存放客户端Socket的管理Map集合
private Map<String, ClientManager> ClientManagers = null;
public static void main(String[] args) {
new Server().startServer();// 启动服务端
}
// 开启服务器方法
private void startServer(){
ServerSocket ss = null;// 服务器Socket
Socket s = null;// 与客户端交互的Socket
try {
ss = new ServerSocket(8888);// 创建服务器,端口为8888
ClientManagers = new HashMap<String, Server.ClientManager>();// 创建管理集合
System.out.println("创建服务器成功!");
while(true){
s = ss.accept();// 接收客户端请求Socket管道
ClientManager cm = new ClientManager(s);// 新建线程Runnable
new Thread(cm).start();// 构建新线程管理该客户端Socket
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(ss != null) ss.close();// 关闭服务器
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 管理客户端Socket线程内部类
private class ClientManager implements Runnable {
private Socket clientSocket;// 客户端Socket
private BufferedReader br;// 基于客户端Socket的输入流(从客户端输入服务端)
private PrintWriter pw;// 基于客户端Socket的输出流(服务端输出到客户端)
private boolean flag;// 线程可用标识
private String physicalAddress;// 客户端名
private String userName;// 用户名
// 构造方法
public ClientManager(Socket s) throws IOException{
this.clientSocket = s;// 获取客户端Socket
br = new BufferedReader(new InputStreamReader(s.getInputStream()));// 基于客户端Socket创建输入流
pw = new PrintWriter(s.getOutputStream(), true);// 基于客户端Socket创建输出流
physicalAddress = s.getInetAddress().getHostAddress()+":"+s.getPort();// 获取客户端Socket物理地址:端口
userName = br.readLine();// 获取用户名
flag = true;// 标识可用
System.out.println(userName+"["+physicalAddress+"]"+"已经成功连接到服务器!");// 客户端连接服务器成功消息发送给所有在线用户
sendMessageForAll("已经成功连接到服务器!");// 客户端连接服务器成功消息发送给所有在线用户
ClientManagers.put(userName, this);// 将自身加入全局客户端线程管理中
sendOnlineList(false);// 发送当前所有在线用户的列表
}
/*
* 发送当前所有在线用户的列表
* 参数IsOnly-->true:发送给当前用户;false:发送给所有在线用户;
*/
private void sendOnlineList(boolean IsOnly) {
if(IsOnly){
StringBuilder sb = new StringBuilder(KeyWords.ONLINE_CLIENTS_LIST + "所有人");// 在线用户列表前缀
for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍历在线用户Socket集合
if(userName.equals(item.getKey())) continue;// 不加入自身
sb.append("," + item.getKey());// 添加其它在线用户
}
pw.println(sb);// 发送列表给当前用户
}else{
for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍历在线用户Socket集合
StringBuilder sb = new StringBuilder(KeyWords.ONLINE_CLIENTS_LIST);// 在线用户列表前缀
sb.append(mapKeyToStringBesidesItem(ClientManagers, item.getKey()));// 题头基础上加上各项
item.getValue().pw.println(sb);// 发送列表给所有用户
}
}
}
// 输入Map<String, ClientManager>后输出除了指定字符串外其余的用","连接的字符串
public String mapKeyToStringBesidesItem(Map<String, ClientManager> stringMap, String besides){
if (stringMap==null) {// 如果输入的Map集合为空,则不进行操作
return null;
}
StringBuilder result = new StringBuilder();// 结果字符串Builder
boolean flag = false;// 是否使用逗号拼接标记
for (Map.Entry<String, ClientManager> item : stringMap.entrySet()) {// 遍历Map集合
if(item.getKey().equals(besides)) continue;// 如果存在排除的字符串则不进行添加操作
if (flag) {// 使用逗号进行拼接(不为第一项)
result.append(",");
}else {// 不适用逗号拼接(第一项标记)
flag=true;
}
result.append(item.getKey());// 拼接字符串
}
return result.toString();// 输出字符串
}
// 发送消息给所有在线用户
private void sendMessageForAll(String msg) {
for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍历在线用户Socket集合
if(userName.equals(item.getKey())) continue;// 不发送给自身
item.getValue().pw.println(userName+"["+physicalAddress+"]:"+msg);// 加上消息头发送
}
}
// 发送消息给指定用户
private void sendMessageForSomeOne(List<String> userList, String msg){
for(Map.Entry<String, ClientManager> item : ClientManagers.entrySet()){// 遍历在线用户Socket集合
if(userList.contains(item.getKey())) item.getValue().pw.println(userName+"["+physicalAddress+"]:"+msg);// 加上消息头发送
}
}
// TODO 接收信息方法
private void receiveMessage() throws IOException{
String str = null;// 临时接收信息变量
while ((str = br.readLine()) != null){// 循环接收客户端信息
if(str.equals(KeyWords.CLIENT_CLOSE_POST)){// 客户端请求断开连接
stopThread();// 停止线程
pw.println(KeyWords.CLIENT_CLOSE_POST_RETURN);// 返回客户端断开确认
break;// 结束循环
} else if(str.startsWith(KeyWords.SEND_TO_TARGET_START)){// 接收消息对象列表前缀
// 分割出发送对象列表
String[] tempSendTo = str.substring(KeyWords.SEND_TO_TARGET_START.length(), str.indexOf(KeyWords.SEND_TO_TARGET_END)).split(",");
List<String> sendTo = Arrays.asList(tempSendTo);
// 获取发送的消息
String msg = str.substring(str.indexOf(KeyWords.SEND_TO_TARGET_END) + KeyWords.SEND_TO_TARGET_END.length());
if(Integer.valueOf(1).equals(tempSendTo.length)){// 如果发送对象只有一个
if("所有人".equals(tempSendTo[0])){// 发送给所有人
sendMessageForAll(msg);
}else{// 真的只有一个发送目标
sendMessageForSomeOne(sendTo, msg);
}
}else{// 发送目标不止一个
sendMessageForSomeOne(sendTo, msg);
}
} else if(str.startsWith(KeyWords.GET_ONLINE_CLIENTS_LIST)){// 请求获取在线客户端列表前缀
sendOnlineList(true);
}
System.out.println("收到"+userName+"["+physicalAddress+"]"+"发来的消息:"+str);// 客户端消息传递日志
}
System.out.println(userName+"["+physicalAddress+"]"+"已经断开与服务器的连接!");// 客户端断开连接日志
stopThread();// 断开了连接则需要将此线程移除
}
// 停止该进程的方法
private void stopThread(){
flag = false;// 标识为空
}
@Override
public void run() {
try {
while (true) {
if(!flag) break;// 如果标识为空则结束循环
receiveMessage();// 调用接收消息方法
}
} catch (SocketException e){
stopThread();// 停止线程
System.out.println(userName+"["+physicalAddress+"]"+"通过非常规方式断开了与服务器的连接!");// 客户端强制关闭日志
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
sendMessageForAll("已经断开与服务器的连接!");// 客户端断开服务器的连接消息发送给所有在线用户
if(clientSocket != null) clientSocket.close();// 关闭客户端连接
ClientManagers.remove(userName);// 将线程自身从全局客户端线程管理中移除
sendOnlineList(false);// 发送当前所有在线用户的列表
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Client.Java为客户端部分,输入与接收分线程实现,从而输入之余能够显示聊天信息交互,以及一些特殊操作关键词过滤等功能。
package cn.com.dnyy.tcp;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
public class Client {
private Socket linkServerSocket;// 与服务器通信的Socket管道
private BufferedReader br;// 接收服务端输入
private PrintWriter pw;// 对服务端输出
private BufferedReader ubr = null;// 接收用户控制台输入
private String userName = null;// 用户名
private boolean flag = true;// 接收线程可用标识
private List<String> onlineUserName;// 在线用户列表
private List<String> sendToList;// 发送的目标用户列表
public static void main(String[] args) {
new Client().startUp();// 启动客户端
}
// 启动客户端
private void startUp(){
try {
linkServerSocket = new Socket("127.0.0.1", 8888);// 连接服务器IP和端口
ubr = new BufferedReader(new InputStreamReader(System.in));// 通过InputStreamReader转换流从字节输入流InputStream转换为BufferedReader
br = new BufferedReader(new InputStreamReader(linkServerSocket.getInputStream()));// 通过服务器Socket创建输入流
pw = new PrintWriter(linkServerSocket.getOutputStream(), true);// 通过服务器Socket创建输出流
onlineUserName = new ArrayList<String>();// 构建新用户列表
sendToList = new ArrayList<String>();// 构建新的发送目标用户列表
System.out.println("连接服务器通信正常!");// 提示客户端连接正常
System.out.println("=========请输入您的用户名:=========");// 提示用户输入用户名信息
userName = ubr.readLine();// 获取用户输入的用户名
pw.println(userName);// 发送用户名给服务器
System.out.println("=========我们欢迎您的到来!=========");
System.out.println("输入\"-M\"显示菜单功能提示");
System.out.println("输入消息并按回车键进行消息发送");
System.out.println("=========预祝您使用愉快!!=========");
new Thread(new ReceiveMessage()).start();// 启动接收线程
String str = null;// 记录读取到的用户输入的字符内容
while ((str = ubr.readLine()) != null) {// TODO 循环读取用户输入数据
if(!flag) break;
if("-M".equals(str)){// 显示菜单
showMainMenu();
}else if("-U".equals(str)){// 显示用户列表
showUserList();
}else if("-Q".equals(str)){// 断开连接
pw.println(KeyWords.CLIENT_CLOSE_POST);
}else if("-A".equals(str)){// 设置消息接收人为所有人
sendToList.clear();// 清空消息接收人
sendToList.add("所有人");// 设置消息接收人为所有人
}else if("-I".equals(str)){// 查看当前接收人列表
showReceiverList();
}else if(str.startsWith("-S,")){// 添加接收人
String tempAddName = str.substring(str.indexOf(",") + 1);// 获取要添加的用户名
if(onlineUserName.contains(tempAddName)) {// 如果存在该用户
sendToList.add(tempAddName);// 添加到接收名单中
System.out.println("接收人[" + tempAddName + "]添加成功!");
if(sendToList.contains("所有人")) sendToList.remove("所有人");// 如果存在“所有人”选项,则去掉
}
}else{// 发送消息
StringBuilder sendTo = new StringBuilder(KeyWords.SEND_TO_TARGET_START);// 消息对象列表前缀
if(sendToList.size() < 1){// 如果发送目标列表小于1,即无发送列表
sendToList.add("所有人");// 群聊
}
for (int i = 0; i < sendToList.size(); i++) {// 加入接收用户列表
if(i < sendToList.size() - 1){
sendTo.append(sendToList.get(i)+",");
}else{
sendTo.append(sendToList.get(i));
}
}
sendTo.append(KeyWords.SEND_TO_TARGET_END+str);// 接收用户列表后缀+内容
pw.println(sendTo);// 传输用户写入的数据到服务器
}
}
} catch (SocketException e){
flag = false;
System.out.println("服务器正在维护中!请稍后重试登陆!");
System.out.println("=========请按回车关闭程序!=========");
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(linkServerSocket != null) linkServerSocket.close();// 关闭服务端连接
} catch (IOException e) {
e.printStackTrace();
}
try {
if(ubr != null) ubr.close();// 关闭用户输入流
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 显示主菜单
private void showMainMenu() {
System.out.println("======================================");
System.out.println("=|[-U]用户列表|[-I]接收人列表|[-Q]安全退出|=");
}
// 显示用户列表
private void showUserList() {
StringBuilder sb = new StringBuilder("==============用户列表=============");// 分隔符
sb.append("\n");// 换行
for(int i = 0; i < onlineUserName.size(); i++){// 循环遍历在想用户列表
sb.append(onlineUserName.get(i));// 输出名字
sb.append("\n");// 换行
}
sb.append("=================================");// 分隔符
sb.append("输入\"-A\"清空接收人并设置为发送给所有人\n");
sb.append("输入\"-S,接收人昵称\"添加消息接收人");
System.out.println(sb);// 输出列表到控制台
}
// 查看当前接收人列表
private void showReceiverList() {
StringBuilder sb = new StringBuilder("==============接收列表=============");// 分隔符
sb.append("\n");// 换行
for(int i = 0; i < sendToList.size(); i++){// 循环遍历在想用户列表
sb.append(sendToList.get(i));// 输出名字
sb.append("\n");// 换行
}
sb.append("=================================");// 分隔符
sb.append("输入\"-A\"清空接收人并设置为发送给所有人\n");
sb.append("输入\"-S,接收人昵称\"添加消息接收人");
System.out.println(sb);// 输出列表到控制台
}
// TODO 接收消息
private void receive() {
try {
String str = br.readLine();// 读取服务器消息
if(str.equals(KeyWords.CLIENT_CLOSE_POST_RETURN)){// 允许关闭程序标识
flag = false;// 如果收到可以关闭程序标识,则关闭程序
System.out.println("=========请按回车关闭程序!=========");
return;
} else if(str.startsWith(KeyWords.ONLINE_CLIENTS_LIST)){// 在线用户列表前缀标识
onlineUserName.clear();// 同步在线用户前清空之前存在的在线用户列表
String[] tempUserNameList = str.substring(KeyWords.ONLINE_CLIENTS_LIST.length()).split(",");// 将去掉前缀后的字符串进行分割
for(String item : tempUserNameList){// 遍历用户列表
onlineUserName.add(item);// 将遍历项添加到用户列表中
}
for(int i = 0; i < sendToList.size(); i++){// 删除发送目标中不存在的在线用户
if(!onlineUserName.contains(sendToList.get(i))) sendToList.remove(i);
}
if(sendToList.size() < 1){// 如果发送目标列表小于1,即无发送列表
sendToList.add("所有人");// 群聊
}
} else {// 获取消息
System.out.println(str);// 输出消息
}
} catch (SocketException e){
flag = false;
System.out.println("服务器正在维护中!请稍后重试登陆!");
System.out.println("=========请按回车关闭程序!=========");
} catch (IOException e) {
e.printStackTrace();
}
}
// 接收服务器信息线程
private class ReceiveMessage implements Runnable {
@Override
public void run() {
while(true){
if(!flag) break;
receive();
}
}
}
}
KeyWords.Java文件主要是服务器和客户端交互的关键字记录文件。
package cn.com.dnyy.tcp;
public class KeyWords {
public final static String CLIENT_CLOSE_POST = "quit:";// 客户端请求断开连接
public final static String CLIENT_CLOSE_POST_RETURN = "disconnect:";// 客户端请求断开连接返回值
public final static String GET_ONLINE_CLIENTS_LIST = "getonline:";// 向服务端请求在线客户端列表前缀
public final static String ONLINE_CLIENTS_LIST = "onlinelist:";// 服务端发送在线客户端列表前缀
public final static String SEND_TO_TARGET_START = "sendto:";// 消息中接收目标列表前缀
public final static String SEND_TO_TARGET_END = ":end";// 消息中接收目标列表后缀
}
文件就这3个,内容也非常之简单,注释我也写得十分的详尽,如果大家有什么疑问的话,敬请在下方留言,我会一一给大家答疑清楚的,喜欢的话请关注点赞,谢谢支持啦~