Socket做一个局域网聊天工具
首先创建一个序列化流类SerialiableMessage
此序列化消息定义了传输消息结构,主要有五个成员变量,分别记录如下信息:
private String sendUserNmae;--发送端用户名
private String sendIp;--发送端ip
private String receiveUserName;--接收端用户名
private String receiveIp;接收端ip
private String message;--消息内容
代码如下:
package com.Test.com;
import java.io.Serializable;
public class SerialiableMessage implements Serializable {
private String sendUserNmae;
private String sendIp;
private String receiveUserName;
private String receiveIp;
private String message;
public SerialiableMessage(String sendUserNmae, String sendIp, String receiveUserName, String message) {
this.sendUserNmae = sendUserNmae;
this.sendIp = sendIp;
this.receiveUserName = receiveUserName;
this.message = message;
}
public String getSendUserNmae() {
return sendUserNmae;
}
public void setSendUserNmae(String sendUserNmae) {
this.sendUserNmae = sendUserNmae;
}
public String getSendIp() {
return sendIp;
}
public void setSendIp(String sendIp) {
this.sendIp = sendIp;
}
public String getReceiveUserName() {
return receiveUserName;
}
public void setReceiveUserName(String receiveUserName) {
this.receiveUserName = receiveUserName;
}
public String getReceiveIp() {
return receiveIp;
}
public void setReceiveIp(String receiveIp) {
this.receiveIp = receiveIp;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public SerialiableMessage() {
}
public SerialiableMessage(String sendUserNmae, String sendIp, String receiveUserName, String receiveIp, String message) {
this.sendUserNmae = sendUserNmae;
this.sendIp = sendIp;
this.receiveUserName = receiveUserName;
this.receiveIp = receiveIp;
this.message = message;
}
}
接下来先定义Client类
包含客户端主方法,获取服务器连接<接收服务端广播来的ip请求连接>,打印好友列表<当前我任意添加了几个用户在列表>,启动线程等方法
package com.Test.com;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Client {
static HashMap<String, String> friendMap = new HashMap<>();
public static void main(String[] args) throws IOException, InterruptedException {
Scanner sc=new Scanner(System.in);
while (true){
client();
System.out.println("是否继续?[Y/N]");
String s = sc.nextLine();
if (s.equals("N")) {
break;
}
}
}
public static void client() throws IOException, InterruptedException {
ArrayList<Boolean>threadState=new ArrayList<Boolean>();
threadState.add(true);
Scanner sc = new Scanner(System.in);
DatagramSocket serveMachineIpdatagramSocket=new DatagramSocket(5050);
byte[]bys=new byte[1024];
DatagramPacket serveMachineIpdatagramPacket=new DatagramPacket(bys,bys.length);
System.out.println("等待服务器响应...");
serveMachineIpdatagramSocket.receive(serveMachineIpdatagramPacket);
String serveMachineIp = new String(bys, 0, bys.length);
Socket socket = new Socket(serveMachineIp, 8000);
printFriendMap();
System.out.println("请选择好友聊天");
String receiveUserName = sc.next();
String receiveIp = friendMap.get(receiveUserName);
ClientSendRunnable clientSendRunnable = new ClientSendRunnable(socket, receiveUserName, receiveIp,threadState);
ClientReceiveRunnable clientReceiveRunnable = new ClientReceiveRunnable(socket, friendMap,threadState);
System.out.println("开始聊天!");
creatChat(clientSendRunnable, clientReceiveRunnable);
}
public static void creatChat(ClientSendRunnable clientSendRunnable, ClientReceiveRunnable clientReceiveRunnable) throws InterruptedException {
Thread clientSendThread = new Thread(clientSendRunnable);
clientSendThread.start();
Thread clientReceiveThread = new Thread(clientReceiveRunnable);
clientReceiveThread.start();
clientSendThread.join();
clientReceiveThread.join();
}
public static void printFriendMap() {
friendMap.put("ewew", "192.168.13.45");
friendMap.put("eatr", "192.168.13.45");
friendMap.put("uiik", "192.168.13.45");
friendMap.put("mfyf", "192.168.13.45");
System.out.println("好友列表!");
System.out.println("**********************************");
System.out.println("用户名\t\t\tip");
for (Map.Entry<String, String> entry : friendMap.entrySet()) {
System.out.println(entry.getKey() + "\t" + entry.getValue());
}
System.out.println("**********************************");
}
}
然后是客服端发送线程类ClientSendRunnable
此类我加入了一个ArrayList state 集合,state中只存放一个元素,用于同步客户端发送线程和接收线程的状态,为了使两个线程都能访问到这个状态,我定义的是ArrayList(当然在Client类中定义一个普通成员变量用static关键字也可以,当时我没想到),state的作用:当Send线程请求关闭应先修改state状态,receive线程访问此到状态也应将自身终结.
package com.Test.com;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.ArrayList;
public class ClientSendRunnable implements Runnable {
private Socket socket;
private String receiveUserName;
private String receiveIp;
ArrayList<Boolean> state;
public ClientSendRunnable(Socket socket, String receiveUserName, String receiveIp,ArrayList<Boolean>state) {
this.socket = socket;
this.receiveUserName = receiveUserName;
this.receiveIp = receiveIp;
this.state=state;
}
@Override
public void run() {
try {
ObjectOutputStream objectOutputStream=new ObjectOutputStream(socket.getOutputStream());
SerialiableMessage serialiableMessage=new SerialiableMessage();
serialiableMessage.setSendUserNmae(socket.getLocalAddress().getHostName());
serialiableMessage.setSendIp(socket.getLocalAddress().getHostName());
serialiableMessage.setReceiveUserName(receiveUserName);
serialiableMessage.setReceiveIp(receiveIp);
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(System.in));
String inLine;
System.out.println("请输入聊天信息!");
System.out.println("*****************");
while ((inLine=bufferedReader.readLine())!=null){
serialiableMessage.setMessage(inLine);
objectOutputStream.writeObject(serialiableMessage);
System.out.println("[to]"+serialiableMessage.getReceiveUserName());
System.out.println("[message]:"+serialiableMessage.getMessage());
objectOutputStream.reset();
if (inLine.equals("exit")) {
state.set(0,false);
serialiableMessage.setReceiveUserName(serialiableMessage.getSendUserNmae());
serialiableMessage.setReceiveIp(serialiableMessage.getSendIp());
objectOutputStream.writeObject(serialiableMessage);
objectOutputStream.reset();
objectOutputStream.close();
break;
}
System.out.println("发送成功!\n");
System.out.println("*****************");
System.out.println("请输入聊天信息!");
}
Thread.sleep(1000);
socket.shutdownOutput();
socket.close();
} catch (IOException e) {
} catch (InterruptedException e) {
}
}
}
客户端接收线程类ClientReceiveRunnable
客户端接收线程每收到消息便判断是否为用户列表,如果为用户列表则更新本地用户列表,否则打印消息
package com.Test.com;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
public class ClientReceiveRunnable implements Runnable{
private Socket socket;
private HashMap<String,String>friendMap;
ArrayList<Boolean> threadState;
public ClientReceiveRunnable(Socket socket, HashMap<String, String> friendMap,ArrayList<Boolean>threadState) {
this.socket = socket;
this.friendMap = friendMap;
this.threadState=threadState;
}
@Override
public void run() {
try {
System.out.println("客户端开始接收消息!");
ObjectInputStream objectInputStream=new ObjectInputStream(socket.getInputStream());
SerialiableMessage serialiableMessage;
while ((serialiableMessage=(SerialiableMessage)objectInputStream.readObject())!=null){
if(threadState.get(0).equals(false)){
objectInputStream.close();
break;
}
if (serialiableMessage.getMessage().equals("accountList")) {
if (serialiableMessage.getReceiveUserName().equals("login")) {
friendMap.put(serialiableMessage.getSendUserNmae(),serialiableMessage.getSendIp());
}else {
friendMap.remove(serialiableMessage.getSendUserNmae());
}
}else {
System.out.println("收到消息!");
System.out.println("[from] "+serialiableMessage.getSendUserNmae());
System.out.println("[message]: "+serialiableMessage.getMessage());
}
}
} catch (IOException e) {
} catch (ClassNotFoundException e) {
}
}
}
下面定义服务器类ServerMachine
此类包含主方法,监听来访的客户端请求,获取局域网本地在线用户等方法,当监听到新客户端请求连接,便将新用户添加到在线用户列表,服务器发送线程每隔一段时间会将在线用户列表更新给所有客户端
package com.Test.com;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class ServerMachine{
public static void ServerMachine() throws IOException {
ArrayList<String> allLocalIp = GainAllLocalUSER.ScannerAllLocalIP();
String serveMachineip = InetAddress.getLocalHost().getHostAddress();
LinkedList<SerialiableMessage> accountLists=new LinkedList<SerialiableMessage>();
ServerSocket serverMachine=new ServerSocket(8000);
System.out.println("服务器启动,开始监听8000端口!");
while (true) {
ServeBordcast serveBordcast=new ServeBordcast(allLocalIp,serveMachineip);
Thread bordServeMachineIp=new Thread(serveBordcast);
bordServeMachineIp.start();
Socket accept = serverMachine.accept();
InetAddress userInetAddress = accept.getInetAddress();
String userName = userInetAddress.getHostName();
SerialiableMessage serialiableUser = new SerialiableMessage(userName, userInetAddress.getHostAddress(),"login", "accountList");
accountLists.add(serialiableUser);
ServerReceiveRunnable serverReceiveRunnable = new ServerReceiveRunnable(accept);
ServerSendRunnable serverSendRunnable = new ServerSendRunnable(accept,accountLists);
Thread receiveThread = new Thread(serverReceiveRunnable);
Thread sendThread = new Thread(serverSendRunnable);
sendThread.start();
receiveThread.start();
}
}
public static void main(String[] args) throws IOException {
ServerMachine();
}
public static class GainAllLocalUSER {
public static ArrayList<String> ScannerAllLocalIP() throws IOException {
ArrayList<String> ipList=new ArrayList();
Runtime runtime=Runtime.getRuntime();
Process process=runtime.exec("arp -a");
StringBuilder stringBuilder=new StringBuilder();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(process.getInputStream(),"GBK"));
String line;
while ((line=bufferedReader.readLine())!=null){
if (line.length()==0) {
int count=0;
while (count<3){
line=bufferedReader.readLine();
count++;
}
}
String[] split = line.split(" ");
ipList.add(split[2]);
}
return ipList;
}
public static ArrayList<String> ScannerAllLocalAccount(ArrayList<String> ipList) throws UnknownHostException {
ArrayList<String> accountList=new ArrayList<>();
for (int i = 0; i <ipList.size() ; i++) {
InetAddress account = InetAddress.getByName(ipList.get(i));
accountList.add(account.getHostName());
}
Iterator<String> iterator=accountList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
return accountList;
}
public static ArrayList<String> ScannerAllLocalAccount() throws IOException {
ArrayList<String> ipList = GainAllLocalUSER.ScannerAllLocalIP();
ArrayList<String> accountList=new ArrayList<>();
for (int i = 0; i <ipList.size() ; i++) {
InetAddress account = InetAddress.getByName(ipList.get(i));
System.out.println(account.getHostName());
accountList.add(account.getHostName());
}
Iterator<String>iterator=accountList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println("返回本地用户ip组成功!");
return accountList;
}
}
}
下面定义服务器消息队列MessageList
由于考虑到局域网内会有多个用户,于是把客户端也分为发送线程和接收进程
因此需要定义一个公用内存区用于临时存放所有用户消息,此MessageList 是序列化消息SerialiableMessage的组成的LinkList集合
package com.Test.com;
import java.util.LinkedList;
public class MessageList {
static LinkedList<SerialiableMessage>messagesLinkList=new LinkedList<>();
public static LinkedList<SerialiableMessage> gainMessageList(){
return messagesLinkList;
}
}
下面定义服务器发送线程类ServerSendRunnable
此线程从MessageList集合死循环遍历,从消息队列的取出记录的receiveIp值,并将receiveIp与此线程连接的客户端ip比较,如果匹配上,则说明此消息是当前线程应该发的消息,于是当前线程发送此消息后从消息队列删除此消息.此线程每隔一段时间向局域网所有客户端发送在线用户列表<此时message内容修改为accountList,标识这是用户列表>
package com.Test.com;
import com.Test.com.MessageList;
import com.Test.com.SerialiableMessage;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.LinkedList;
public class ServerSendRunnable implements Runnable {
private Socket accept;
private LinkedList<SerialiableMessage> accountLists;
public ServerSendRunnable(Socket accept, LinkedList<SerialiableMessage> accountLists) {
this.accept = accept;
this.accountLists = accountLists;
}
@Override
public void run() {
try {
System.out.println("*为"+accept.getInetAddress().getHostName()+"创建了一条发送线程!");
ObjectOutputStream objectOutputStream=new ObjectOutputStream(accept.getOutputStream());
String ip = accept.getInetAddress().getHostAddress();
long starttime = System.currentTimeMillis();
while (true){
long endtime = System.currentTimeMillis();
if ((endtime-starttime)%60000<5000) {
for (int i = 0; i <accountLists.size() ; i++) {
objectOutputStream.writeObject(accountLists.get(i));
}
}
LinkedList<SerialiableMessage> messagesLinkList = MessageList.gainMessageList();
for (int i = 0; i <messagesLinkList.size() ; i++) {
SerialiableMessage serialiableMessage = messagesLinkList.get(i);
if (serialiableMessage.getMessage().equals("exit")) {
for (int j = 0; j <accountLists.size() ; j++) {
if (accountLists.get(j).getSendUserNmae().equals(serialiableMessage.getSendUserNmae())) {
accountLists.remove(j);
objectOutputStream.close();
break;
}
}
messagesLinkList.remove(i);
break;
}
if (ip.equals(serialiableMessage.getReceiveIp())) {
objectOutputStream.writeObject(serialiableMessage);
messagesLinkList.remove(i);
}
}
}
} catch (IOException e) {
}
}
}
下面定义服务器接受端线程类ServerReceiveRunnable
此线程从客户端收到消息后就将消息存入消息队列,让发送线程来取出发送
package com.Test.com;
import com.Test.com.MessageList;
import com.Test.com.SerialiableMessage;
import java.io.*;
import java.net.Socket;
import java.util.LinkedList;
public class ServerReceiveRunnable implements Runnable{
private Socket accept;
public ServerReceiveRunnable(Socket accept) {
this.accept = accept;
}
@Override
public void run() {
try {
System.out.println("*为"+accept.getInetAddress().getHostName()+"创建了一条接收线程!");
ObjectInputStream objectInputStream=new ObjectInputStream(accept.getInputStream());
SerialiableMessage messageLine;
System.out.println("**********************************\n");
/*****************************************/
while ((messageLine =(SerialiableMessage)objectInputStream.readObject()) != null) {
System.out.println("[from] "+messageLine.getSendUserNmae()+" [to] "+messageLine.getReceiveUserName());
System.out.println("[message]: "+messageLine.getMessage());
LinkedList<SerialiableMessage> messagesLinkList = MessageList.gainMessageList();
messagesLinkList.add(messageLine);
System.out.println("\n");
System.out.println("**********************************\n");
}
objectInputStream.close();
} catch (IOException e) {
} catch (ClassNotFoundException e) {
}finally {
try {
accept.close();
} catch (IOException e) {
}
}
}
}
下面这个线程类ServeBordcast是我后面增加的
考虑到服务端是不同的电脑,并且每次重启电脑后ip可能不一样,于是增加这个服务端线程类,此线程向局域网在线客户端隔一段时间便使用UDP协广播服务端ip(广播服务添加后未经测试,在同一台电脑同时测试客户端和服务端可能会存在多线程抢占访问本地IP冲突,如果这样,停止在ServerMachine类启动此线程,并在Client类client()方法中删掉等待服务器响应那一段,并将"Socket socket = new Socket(serveMachineIp, 8000);"中serveMachineIp修改为实际服务器ip即可)
package com.Test.com;
import java.io.IOException;
import java.net.*;
import java.util.ArrayList;
public class ServeBordcast implements Runnable {
private ArrayList<String> allLocalIp;
private String serveMachineIp;
public ServeBordcast(ArrayList<String> allLocalIp, String serveMachineIp) {
this.allLocalIp = allLocalIp;
this.serveMachineIp = serveMachineIp;
}
@Override
public void run() {
while (true) {
for (int i = 0; i < allLocalIp.size(); i++) {
DatagramSocket ds = null;
try {
ds = new DatagramSocket();
} catch (SocketException e) {
}
DatagramPacket dp = null;
try {
dp = new DatagramPacket(serveMachineIp.getBytes(), serveMachineIp.getBytes().length, InetAddress.getByName(allLocalIp.get(i)), 5050);
} catch (UnknownHostException e) {
}
try {
ds.send(dp);
} catch (IOException e) {
}
ds.close();
}
System.out.println("广播服务器ip成功!");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
}
}