一.用户需求:
1.实现使用用户名登陆(不能重复)
2.登陆后能获取所有在线用户
3.可以给所用用户群发信息
4.也可以给某个用户单独发送信息
5.用户退出时,一起登陆用户能立刻感知
二.初步分析:
1.需要在服务器端记录当前登陆的用户,便于用户登陆时用户名查重及消息群发
2.用户登陆成功后需要获取所有在线用户信息,并记录,能够单独给该用户发送信息
3.用户退出时,需要通知服务器从服务器数据中删除该用户并通知到所有用户
三.效果图:
图1.服务器启动Socket监听
图2 启动客户端登录
图3.用户1登录成功
图4.用户1再次登录,提示重复用户名
图5.用户2登录成功,用户1和用户2都能获取到当前登录用户
图6.用户1群发信息,用户1和用户2都能收到群发消息
图7.用户2与用户1私聊
图8用户2与用户1私聊,收到信息“hi,1,I am 2”
图9.用户1退出,用户2能感知到
四.源码
自定义消息结构
<span style="color:#000000;">package Common;
public enum Command {
Command,
Text,
}
package Common;
public enum CommandType {
Login,
Quit,
GetMember,
Null
}
package Common;
import java.io.*;
import java.net.Socket;
public class Member {
private String User;
private Socket s;
private DataInputStream dis;
private DataOutputStream dos;
public Member(String name, Socket s, DataInputStream dis,
DataOutputStream dos) {
this.User = name;
this.s = s;
this.dis = dis;
this.dos = dos;
}
public DataInputStream getDis() {
return dis;
}
public DataOutputStream getDos() {
return dos;
}
public String getUser() {
return User;
}
public Socket getS() {
return s;
}
public void setS(Socket s) {
this.s = s;
}
}</span>
自定义消息类
<span style="color:#000000;">package Common; import java.util.*; public class Message { private String from; private String to; private String msgText; private Date sendDate; private Command cmd; private CommandType cmdType; public Message(){ } public Message(String From, String To, String MsgText, Date SendDate, Command CMD,CommandType CMDType) { this.from = From; this.to = To; this.msgText = MsgText; this.sendDate = SendDate; this.cmd = CMD; this.cmdType = CMDType; } public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getTo() { return to; } public void setTo(String to) { this.to = to; } public String getMsgText() { return msgText; } public void setMsgText(String msgText) { this.msgText = msgText; } public Date getSendDate() { return sendDate; } public void setSendDate(Date sendDate) { this.sendDate = sendDate; } public Command getCmd() { return cmd; } public void setCmd(Command cmd) { this.cmd = cmd; } public CommandType getCmdType() { return cmdType; } public void setCmdType(CommandType cmdType) { this.cmdType = cmdType; } }</span>
自定义异常
<span style="color:#000000;">package Common; public class MyException extends Exception { String ErrorMsg; public MyException (String errMsg){ this.ErrorMsg = errMsg; } public String toStr(){ return ErrorMsg; } }</span>
自定义消息处理类
<span style="color:#000000;">package Common; import java.util.*; import java.text.*; import javax.swing.*; public class Utility { public static Message ConvertSting2Msg(String str) throws MyException { Message msg = new Message(); String[] param = new String[6]; int index = str.indexOf("\n"); int paraIndex = 0; while (index > 0) { String temp = str.substring(0, index); param[paraIndex++]=temp; str = str.substring(index + 1); index = str.indexOf("\n"); } param[paraIndex]=str; if (paraIndex != 5) throw new MyException("Parameter number Error!"+paraIndex); msg.setFrom(param[0]); msg.setTo(param[1]); msg.setMsgText(param[2]); SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { Date sendDate =sdf.parse(param[3]); msg.setSendDate(sendDate); } catch (ParseException e) { throw new MyException("Date Format Error!"); } msg.setCmd(Command.valueOf(param[4])); msg.setCmdType(CommandType.valueOf(param[5])); return msg; } public static String ConvertMsg2String(Message msg) { StringBuffer sb = new StringBuffer(); sb.append(msg.getFrom()); sb.append("\n"); sb.append(msg.getTo()); sb.append("\n"); sb.append(msg.getMsgText()); sb.append("\n"); SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String SendDate = sdf.format(msg.getSendDate()); sb.append(SendDate); sb.append("\n"); sb.append(msg.getCmd().toString()); sb.append("\n"); sb.append(msg.getCmdType().toString()); return sb.toString(); } public static void Print(String str){ System.out.println(str); } public static void ShowMsg(String str){ JOptionPane.showMessageDialog(null, str); } }</span>
服务器端类
<span style="color:#000000;">package Main; import Common.*; import java.io.*; import java.net.*; import java.util.*; public class Server { static Server server = new Server(); ServerSocket ss = null; boolean ServerStarted = false; List<Member> Members = new ArrayList<Member>(); MessageHandler mh = null; private Server() { } public static Server GetServerInstance() { if (server == null) { server = new Server(); } return server; } public void StartSvr() { Utility.Print("Server Start to Run from: " + new Date().toString()); try { ss = new ServerSocket(8888); ServerStarted = true; } catch (IOException e) { Utility.ShowMsg("Server Starts failed!Please check whether this is another Server started!"); System.exit(0); } while (ServerStarted) { try { Socket s = ss.accept(); mh = new MessageHandler(s); new Thread(mh).start(); } catch (IOException e) { Utility.Print("server accept failed!"); ServerStarted = false; } } } private boolean IsContents(String user) { int size = Members.size(); for (int i = 0; i < size; i++) { if (Members.get(i).getUser().equals(user)) { return true; } } return false; } private void RemoveMember(String user) { for (int i = 0; i < Members.size(); i++) { if (Members.get(i).getUser().equals(user)) { Members.remove(i); } } } private String MemberToString() { StringBuffer sb = new StringBuffer(""); int size = Members.size(); for (int i = 0; i < size; i++) { sb.append(Members.get(i).getUser() + ":"); } if (sb.length() > 1) { return sb.substring(0, sb.length() - 1); } return sb.toString(); } //内部类,用于服务器处理客户端请求 class MessageHandler implements Runnable { Socket s = null; boolean started = false; DataInputStream dis = null; DataOutputStream dos = null; public MessageHandler(Socket s) { this.s = s; } public void run() { try { dis = new DataInputStream(s.getInputStream()); dos = new DataOutputStream(s.getOutputStream()); started = true; } catch (IOException e) { e.printStackTrace(); } while (started) { try { String InputStr = dis.readUTF(); Utility.Print(InputStr); Message ms = Utility.ConvertSting2Msg(InputStr); MsgDispatch(ms); } catch (IOException e) { started = false; } catch (MyException e) { started = false; Utility.Print(e.toStr()); } } if (!started) { try { if (dis != null) { dis.close(); dis = null; } if (dos != null) { dos.close(); dos = null; } } catch (IOException e) { e.printStackTrace(); } return; } } public void MsgDispatch(Message msg) { if (msg.getCmd() == Command.Command) { if (!msg.getTo().equals("Server")) { started = false; Utility.Print("Error Command Message!"); return; } if (msg.getCmdType() == CommandType.Login) { String user = msg.getFrom(); if (!IsContents(user)) { Member m = new Member(user, s, dis, dos); Members.add(m); Utility.Print("A client add to member list!"); try { Message Outmsg = new Message("Server", msg.getFrom(), "Success", new Date(), Command.Command, CommandType.Login); dos.writeUTF(Utility.ConvertMsg2String(Outmsg)); } catch (IOException e) { e.printStackTrace(); Members.remove(m); } } else { Utility.Print("this client exists!"); try { Message Outmsg = new Message("Server", msg.getFrom(), "failed", new Date(), Command.Command, CommandType.Login); dos.writeUTF(Utility.ConvertMsg2String(Outmsg)); } catch (IOException e) { e.printStackTrace(); } } } else if (msg.getCmdType() == CommandType.Quit) { String user = msg.getFrom(); if (IsContents(user)) { RemoveMember(user); Utility.Print("User " + user + " is removed from member list!"); try { int size = Members.size(); for (int i = 0; i < size; i++) { Message Outmsg = new Message("Server", Members .get(i).getUser(), MemberToString(), new Date(), Command.Command, CommandType.GetMember); Members.get(i).getDos().writeUTF(Utility.ConvertMsg2String(Outmsg)); } } catch (IOException e) { e.printStackTrace(); } return; } } else if (msg.getCmdType() == CommandType.GetMember) { try { for (int i = 0; i < Members.size(); i++) { Message Outmsg = new Message("Server", Members.get( i).getUser(), MemberToString(), new Date(), Command.Command, CommandType.GetMember); Members.get(i).getDos().writeUTF(Utility.ConvertMsg2String(Outmsg)); } } catch (IOException e) { e.printStackTrace(); } } } else { if (msg.getCmdType() != CommandType.Null) { Utility.Print("Error Text Message"); return; } if (msg.getTo().equals("ALL")) { try { for (int i = 0; i < Members.size(); i++) { Message Outmsg = new Message(msg.getFrom(), "ALL", msg.getMsgText(), new Date(), Command.Text, CommandType.Null); Members.get(i).getDos().writeUTF(Utility.ConvertMsg2String(Outmsg)); } } catch (IOException e) { e.printStackTrace(); } } else { try { for (int i = 0; i < Members.size(); i++) { Message Outmsg = null; if (Members.get(i).getUser().equals(msg.getTo()) || Members.get(i).getUser() .equals(msg.getFrom())) { Outmsg = new Message(msg.getFrom(), msg.getTo(), msg.getMsgText(), new Date(), Command.Text, CommandType.Null); Members.get(i).getDos().writeUTF(Utility.ConvertMsg2String(Outmsg)); } } } catch (IOException e) { e.printStackTrace(); } } } } } }</span>
服务器端主类,必须最先运行
<span style="color:#000000;">package Main; public class StartServer { public static void main(String[] args) { Server server = Server.GetServerInstance(); server.StartSvr(); } }</span>
客户端登录类
<span style="color:#000000;">package Main; import Common.*; import java.awt.*; import java.awt.event.*; import java.util.*; import java.io.*; import java.net.*; public class Login extends Frame { private Label la = new Label("UserName:"); private TextField tf = new TextField("", 10); private Button login = new Button("Login"); Socket s = null; boolean connect = false; public void LaunchFrame() { setLocation(200, 300); setSize(300, 100); setResizable(false); setTitle("Login"); setLayout(new FlowLayout()); add(la, FlowLayout.LEFT); add(tf); add(login); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { System.exit(0); } }); login.addActionListener(new LoginAction()); setVisible(true); } class LoginAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { String Username = tf.getText(); if (Username != null && !Username.trim().equals("")) { Message msg = new Message(Username, "Server", "Login", new Date(), Command.Command, CommandType.Login); String Sendmsg = Utility.ConvertMsg2String(msg); Connect(Sendmsg); return; } else { Utility.ShowMsg("UserName can not be Null!"); return; } } private void Connect(String str) { try { if (!connect) { s = new Socket("127.0.0.1", 8888); connect = true; Utility.Print("Connect to Server success!"); } } catch (UnknownHostException e) { Utility.ShowMsg("Can't find the server host!"); return; } catch (IOException e) { e.printStackTrace(); Utility.ShowMsg("Can't connect to the server!"); return; } if (connect) { try { DataOutputStream dos = new DataOutputStream(s.getOutputStream()); dos.writeUTF(str); DataInputStream dis = new DataInputStream(s.getInputStream()); Message msg = Utility.ConvertSting2Msg(dis.readUTF()); if (msg.getCmd() == Command.Command && msg.getCmdType() == CommandType.Login) { if (msg.getMsgText().equals("Success")) { connect = false; new FullChat(s, msg.getTo(),dis,dos); dispose(); } else { Utility.ShowMsg("Duplicate Username!"); } } } catch (IOException e) { Utility.ShowMsg("Can't connect the server!"); } catch (MyException e2) { Utility.ShowMsg("Uncorrect message from server!"); } return; } } } }</span>
客户端群发类
<span style="color:#000000;">package Main; import java.awt.*; import java.awt.List; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import Common.*; public class FullChat extends Frame { TextField tf = new TextField(40); TextArea ta = new TextArea(20, 40); List list = new List(20); private Socket s = null; private String name; DataOutputStream dos = null; DataInputStream dis = null; boolean connected = false; Map<String, AloneChat> aloneChats = new HashMap<String, AloneChat>(); public FullChat(Socket s, String name, DataInputStream dis, DataOutputStream dos) { this.s = s; this.name = name; this.dis = dis; this.dos = dos; connected = true; setTitle(name); setLocation(200, 200); setSize(440, 400); setResizable(false); Panel pl = new Panel(); pl.setPreferredSize(new Dimension(300, 380)); add(pl, BorderLayout.WEST); Panel pr = new Panel(); pr.setPreferredSize(new Dimension(130, 380)); add(pr, BorderLayout.EAST); pl.add(ta); ta.setEditable(false); pl.add(tf); list.select(-1); list.addItemListener(new ListClick()); pr.add(list); tf.addActionListener(new TextFiledAction()); addWindowListener(new WindowClose()); setVisible(true); RequestMember(); new Thread(new FullChatThread()).start(); } private void RequestMember() { // 通知server移除Socket Message msg = new Message(name, "Server", "GetMember", new Date(), Command.Command, CommandType.GetMember); String Sendmsg = Utility.ConvertMsg2String(msg); try { dos.writeUTF(Sendmsg); } catch (IOException e1) { e1.printStackTrace(); } } class ListClick implements ItemListener { @Override public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { String user = list.getItem(list.getSelectedIndex()); if (!aloneChats.containsKey(user)) { if (!user.equals(name)) { AloneChat ac = new AloneChat(s, dis, dos, name, user); aloneChats.put(user, ac); Utility.Print(user + " add to Map!"); ac.Init(); } } else { AloneChat ac = aloneChats.get(user); ac.setVisible(true); Utility.Print("aa"); } } } } class WindowClose extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { // 通知server移除Socket Message msg = new Message(name, "Server", "Quit", new Date(), Command.Command, CommandType.Quit); String Sendmsg = Utility.ConvertMsg2String(msg); try { dos.writeUTF(Sendmsg); } catch (IOException e1) { e1.printStackTrace(); } connected = false; dispose(); } } class TextFiledAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { String SndText = tf.getText(); tf.setText(""); if (SndText.equals("")) { Utility.ShowMsg("Please Input Message"); return; } Message msg = new Message(name, "ALL", SndText, new Date(), Command.Text, CommandType.Null); String Sendmsg = Utility.ConvertMsg2String(msg); try { dos.writeUTF(Sendmsg); } catch (IOException ex) { Utility.Print("Send Message to Server failed"); } } } class FullChatThread implements Runnable { @Override public void run() { while (connected) { try { String str = dis.readUTF(); Utility.Print(str); Message msg = Utility.ConvertSting2Msg(str); if (msg.getCmdType() == CommandType.GetMember && msg.getCmd() == Command.Command) { String[] members = msg.getMsgText().split(":"); list.removeAll(); for (String member : members) { list.add(member); } } else if (msg.getCmd() == Command.Text) { String ChatTo = msg.getTo(); if (ChatTo.equals("ALL")) { String oldContent = ta.getText(); StringBuffer sb = new StringBuffer("\n"); sb.append(msg.getFrom()); sb.append(" "); sb.append(msg.getSendDate()); sb.append(" "); sb.append("对所有人说:"); sb.append("\n"); sb.append(msg.getMsgText()); ta.setText(oldContent + sb.toString()); } else { if (ChatTo.equals(name)) { String ChatFrom = msg.getFrom(); StringBuffer sb = new StringBuffer("\n"); sb.append(msg.getFrom()); sb.append(" "); sb.append(msg.getSendDate()); sb.append(" "); sb.append("对"); sb.append(msg.getTo()); sb.append("说:"); sb.append("\n"); sb.append(msg.getMsgText()); if (!aloneChats.containsKey(ChatFrom)) { AloneChat ac = new AloneChat(s, dis, dos, name, ChatFrom); aloneChats.put(ChatFrom, ac); ac.Init(); ac.ReceivedMsg(sb.toString()); } else { AloneChat ac = aloneChats.get(ChatFrom); ac.setVisible(true); ac.ReceivedMsg(sb.toString()); } }else if(msg.getFrom().equals(name)){ String ChatFrom = msg.getFrom(); StringBuffer sb = new StringBuffer("\n"); sb.append(msg.getFrom()); sb.append(" "); sb.append(msg.getSendDate()); sb.append(" "); sb.append("对"); sb.append(msg.getTo()); sb.append("说:"); sb.append("\n"); sb.append(msg.getMsgText()); AloneChat ac = aloneChats.get(msg.getTo()); ac.ReceivedMsg(sb.toString()); } } } } catch (IOException e) { System.exit(0); } catch (MyException e) { e.printStackTrace(); connected = false; } } } } }</span>
客户端主类,起客户端时必须确保服务器主类已经运行,通过运行该类启动登录窗口。
<span style="color:#000000;">package Main; public class Client { public static void main(String[] args) { new Login().LaunchFrame(); } }</span>
私聊窗口类
<span style="color:#000000;">package Main; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.*; import java.net.*; import java.util.*; import Common.Command; import Common.CommandType; import Common.Message; import Common.MyException; import Common.Utility; public class AloneChat extends Frame { Socket s = null; DataInputStream dis = null; DataOutputStream dos = null; String from; String to; TextArea ta = new TextArea(); TextField tf = new TextField(); boolean connected = false; public AloneChat(Socket s, DataInputStream dis, DataOutputStream dos, String from, String to) { this.s = s; this.dis = dis; this.dos = dos; this.from = from; this.to = to; } public void Init() { connected = true; setLocation(500, 300); setSize(300, 300); setTitle(from + " and " + to + " begains chat.."); add(ta, BorderLayout.NORTH); add(tf, BorderLayout.SOUTH); ta.setEditable(false); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { dispose(); } }); tf.addActionListener(new TextFiledAction()); pack(); setVisible(true); setResizable(false); } public void ReceivedMsg(String msg){ String oldContent = ta.getText(); ta.setText(oldContent+msg); } class TextFiledAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { String SndText = tf.getText(); tf.setText(""); if (SndText.equals("")) { Utility.ShowMsg("Please Input Message"); return; } Message msg = new Message(from, to, SndText, new Date(), Command.Text, CommandType.Null); String Sendmsg = Utility.ConvertMsg2String(msg); try { dos.writeUTF(Sendmsg); } catch (IOException ex) { Utility.Print("Send Message to Server failed"); } } } }</span>
五.不足之处
1.在服务器端用容器存储当前用户,没有做数据同步处理,在多线程情况下回数据不一致,不过在单机上测试没问题,对学习Socket没多大影响
2.数据没有作抽象处理,仅仅只是从代码上实现了功能,有待优化
3.在启服务器监听时使用了单例模式,但在client端聊天时<私聊>没有使用观察者模式
4.仅仅是一时兴起,想回顾一下Socket编程和多线程编程,对内部原理及相关机制还有待进一步学习.
5.由于一时兴起,也没有对本项目仔细分析,从软件工程和面向对象角度进行思考
本文只是对自己学习过程的记忆,也希望能对初学者有所帮助。(2013-04)
时间: 2024-10-11 13:29:21