Java---基于TCP协议的相互即时通讯小程序
Posted 谙忆
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java---基于TCP协议的相互即时通讯小程序相关的知识,希望对你有一定的参考价值。
这是几年前,新浪的一个面试题~要求是3天之内实现~
通过TCP 协议,建立一个服务器端。
通过配置服务器端的IP和端口:
客户端之间就可以相互通讯~
上线了全部在线用户会收到你上线的通知。
下线了全部的在线用户会收到你下线的通知!
可以私聊,可以群聊。
这是第一个版本~以后有空可以再增加功能~比如传文件啊~等等~
设计思想如下:
在服务器端 用一个HashMap< userName,socket> 维护所有用户相关的信息,从而能够保证和所有的用户进行通讯。
客户端的动作:
(1)连接(登录):发送userName 服务器的对应动作:1)界面显示,2)通知其他用户关于你登录的信息, 3)把其他在线用户的userName通知当前用户 4)开启一个线程专门为当前线程服务
(2)退出(注销):
(3)发送消息
※※发送通讯内容之后,对方如何知道是干什么,通过消息协议来实现:
客户端向服务器发的消息格式设计:
命令关键字@#接收方@#消息内容@#发送方
1)连接:userName —-握手的线程serverSocket专门接收该消息,其它的由服务器新开的与客户进行通讯的socket来接收
2)退出:exit@#全部@#null@#userName
3)发送: on @# JList.getSelectedValue() @# tfdMsg.getText() @# tfdUserName.getText()
服务器向客户端发的消息格式设计:
命令关键字@#发送方@#消息内容
登录:
1) msg @#server @# 用户[userName]登录了 (给客户端显示用的)
2) cmdAdd@#server @# userName (给客户端维护在线用户列表用的)
退出:
1) msg @#server @# 用户[userName]退出了 (给客户端显示用的)
2) cmdRed@#server @# userName (给客户端维护在线用户列表用的)
发送:
msg @#消息发送者( msgs[3] ) @# 消息内容 (msgs[2])
服务器端源代码:
package cn.hncu;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
/**
* 服务器
*
* @author 陈浩翔
*
* @version 1.0 2016-5-13
*/
public class ServerForm extends JFrame {
private JList<String> list;
private JTextArea area;
private DefaultListModel<String> lm;
public ServerForm() {
JPanel p = new JPanel(new BorderLayout());
// 最右边的用户在线列表
lm = new DefaultListModel<String>();
list = new JList<String>(lm);
JScrollPane js = new JScrollPane(list);
Border border = new TitledBorder("在线");
js.setBorder(border);
Dimension d = new Dimension(100, p.getHeight());
js.setPreferredSize(d);// 设置位置
p.add(js, BorderLayout.EAST);
// 通知文本区域
area = new JTextArea();
//area.setEnabled(false);//不能选中和修改
area.setEditable(false);
p.add(new JScrollPane(area), BorderLayout.CENTER);
this.getContentPane().add(p);
// 添加菜单项
JMenuBar bar = new JMenuBar();// 菜单条
this.setJMenuBar(bar);
JMenu jm = new JMenu("控制(C)");
jm.setMnemonic('C');// 设置助记符---Alt+'C',显示出来,但不运行
bar.add(jm);
final JMenuItem jmi1 = new JMenuItem("开启");
jmi1.setAccelerator(KeyStroke.getKeyStroke('R', KeyEvent.CTRL_MASK));// 设置快捷键Ctrl+'R'
jmi1.setActionCommand("run");
jm.add(jmi1);
JMenuItem jmi2 = new JMenuItem("退出");
jmi2.setAccelerator(KeyStroke.getKeyStroke('E', KeyEvent.CTRL_MASK));// 设置快捷键Ctrl+'R'
jmi2.setActionCommand("exit");
jm.add(jmi2);
// 监听
ActionListener a1 = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("run")) {
startServer();
jmi1.setEnabled(false);// 内部方法~访问的只能是final对象
} else {
System.exit(0);
}
}
};
jmi1.addActionListener(a1);
Toolkit tk = Toolkit.getDefaultToolkit();
int width = (int) tk.getScreenSize().getWidth();
int height = (int) tk.getScreenSize().getHeight();
this.setBounds(width / 4, height / 4, width / 2, height / 2);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);// 关闭按钮器作用
setVisible(true);
}
private static final int PORT = 9090;
protected void startServer() {
try {
ServerSocket server = new ServerSocket(PORT);
area.append("启动服务:" + server);
new ServerThread(server).start();
} catch (IOException e) {
e.printStackTrace();
}
}
// 用来保存所有在线用户的名字和Socket----池
private Map<String, Socket> usersMap = new HashMap<String, Socket>();
class ServerThread extends Thread {
private ServerSocket server;
public ServerThread(ServerSocket server) {
this.server = server;
}
@Override
public void run() {
try {// 和客户端握手
while (true) {
Socket socketClient = server.accept();
Scanner sc = new Scanner(socketClient.getInputStream());
if (sc.hasNext()) {
String userName = sc.nextLine();
area.append("\\r\\n用户[ " + userName + " ]登录 " + socketClient);// 在客户端通知
lm.addElement(userName);// 添加到用户在线列表
new ClientThread(socketClient).start();// 专门为这个客户端服务
usersMap.put(userName, socketClient);// 把当前登录的用户加到“在线用户”池中
msgAll(userName);// 把“当前用户登录的消息即用户名”通知给所有其他已经在线的人
msgSelf(socketClient);// 通知当前登录的用户,有关其他在线人的信息
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientThread extends Thread {
private Socket socketClient;
public ClientThread(Socket socketClient) {
this.socketClient = socketClient;
}
@Override
public void run() {
System.out.println("一个与客户端通讯的线程启动并开始通讯...");
try {
Scanner sc = new Scanner(socketClient.getInputStream());
while (sc.hasNext()) {
String msg = sc.nextLine();
System.out.println(msg);
String msgs[] = msg.split("@#@#");
//防黑
if(msgs.length!=4){
System.out.println("防黑处理...");
continue;
}
if("on".equals(msgs[0])){
sendMsgToSb(msgs);
}
if("exit".equals(msgs[0])){
//服务器显示
area.append("\\r\\n用户[ " + msgs[3] + " ]已退出!" + usersMap.get(msgs[3]));
//从在线用户池中把该用户删除
usersMap.remove(msgs[3]);
//服务器的在线列表中把该用户删除
lm.removeElement(msgs[3]);
//通知其他用户,该用户已经退出
sendExitMsgToAll(msgs);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//通知其他用户。该用户已经退出
private void sendExitMsgToAll(String[] msgs) throws IOException {
Iterator<String> userNames = usersMap.keySet().iterator();
while(userNames.hasNext()){
String userName = userNames.next();
Socket s = usersMap.get(userName);
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
String str = "msg@#@#server@#@#用户[ "+msgs[3]+" ]已退出!";
pw.println(str);
pw.flush();
str = "cmdRed@#@#server@#@#"+msgs[3];
pw.println(str);
pw.flush();
}
}
//服务器把客户端的聊天消息转发给相应的其他客户端
public void sendMsgToSb(String[] msgs) throws IOException {
if("全部".equals(msgs[1])){
Iterator<String> userNames = usersMap.keySet().iterator();
//遍历每一个在线用户,把聊天消息发给他
while(userNames.hasNext()){
String userName = userNames.next();
Socket s = usersMap.get(userName);
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
String str = "msg@#@#"+msgs[3]+"@#@#"+msgs[2];
pw.println(str);
pw.flush();
}
}else{
Socket s = usersMap.get(msgs[1]);
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
String str = "msg@#@#"+msgs[3]+"对你@#@#"+msgs[2];
pw.println(str);
pw.flush();
}
}
/**
* 把“当前用户登录的消息即用户名”通知给所有其他已经在线的人
*
* @param userName
*/
// 技术思路:从池中依次把每个socket(代表每个在线用户)取出,向它发送userName
public void msgAll(String userName) {
Iterator<Socket> it = usersMap.values().iterator();
while (it.hasNext()) {
Socket s = it.next();
try {
PrintWriter pw = new PrintWriter(s.getOutputStream(), true);// 加true为自动刷新
String msg = "msg@#@#server@#@#用户[ " + userName + " ]已登录!";// 通知客户端显示消息
pw.println(msg);
pw.flush();
msg = "cmdAdd@#@#server@#@#" + userName;// 通知客户端在在线列表添加用户在线。
pw.println(msg);
pw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 通知当前登录的用户,有关其他在线人的信息
*
* @param socketClient
*/
// 把原先已经在线的那些用户的名字发给该登录用户,让他给自己界面中的lm添加相应的用户名
public void msgSelf(Socket socketClient) {
try {
PrintWriter pw = new PrintWriter(socketClient.getOutputStream(),true);
Iterator<String> it = usersMap.keySet().iterator();
while (it.hasNext()) {
String msg = "cmdAdd@#@#server@#@#" + it.next();
pw.println(msg);
pw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);// 设置装饰
new ServerForm();
}
}
客户端源代码:
package cn.hncu;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
/**
* 客户端
*
* @author 陈浩翔
* @version 1.0 2016-5-13
*/
public class ClientForm extends JFrame implements ActionListener {
private JTextField tfdUserName;
private JList<String> list;
private DefaultListModel<String> lm;
private JTextArea allMsg;
private JTextField tfdMsg;
private JButton btnCon;
private JButton btnExit;
private JButton btnSend;
// private static String HOST="192.168.31.168";
private static String HOST = "127.0.0.1";// 自己机子,服务器的ip地址
private static int PORT = 9090;// 服务器的端口号
private Socket clientSocket;
private PrintWriter pw;
public ClientForm() {
super("即时通讯工具1.0");
// 菜单条
addJMenu();
// 上面的面板
JPanel p = new JPanel();
JLabel jlb1 = new JLabel("用户标识:");
tfdUserName = new JTextField(10);
// tfdUserName.setEnabled(false);//不能选中和修改
// dtfdUserName.setEditable(false);//不能修改
// 链接按钮
ImageIcon icon = new ImageIcon("a.png");
btnCon = new JButton("", icon);
btnCon.setActionCommand("c");
btnCon.addActionListener(this);
// 退出按钮
icon = new ImageIcon("b.jpg");
btnExit = new JButton("", icon);
btnExit.setActionCommand("exit");
btnExit.addActionListener(this);
btnExit.setEnabled(false);
p.add(jlb1);
p.add(tfdUserName);
p.add(btnCon);
p.add(btnExit);
getContentPane().add(p, BorderLayout.NORTH);
// 中间的面板
JPanel cenP = new JPanel(new BorderLayout());
this.getContentPane().add(cenP, BorderLayout.CENTER);
// 在线列表
lm = new DefaultListModel<String>();
list = new JList<String>(lm);
lm.addElement("全部");
list.setSelectedIndex(0);// 设置默认显示
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);// 只能选中一行
list.setVisibleRowCount(2);
JScrollPane js = new JScrollPane(list);
Border border = new TitledBorder("在线");
js.setBorder(border);
Dimension preferredSize = new Dimension(70, cenP.getHeight());
js.setPreferredSize(preferredSize);
cenP.add(js, BorderLayout.EAST);
// 聊天消息框
allMsg = new JTextArea();
allMsg.setEditable(false);
cenP.add(new JScrollPane(allMsg), BorderLayout.CENTER);
// 消息发送面板
JPanel p3 = new JPanel();
JLabel jlb2 = new JLabel("消息:");
p3.add(jlb2);
tfdMsg = new JTextField(20);
p3.add(tfdMsg);
btnSend = new JButton("发送");
btnSend.setEnabled(false);
btnSend.setActionCommand("send");
btnSend.addActionListener(this);
p3.add(btnSend);
this.getContentPane().add(p3, BorderLayout.SOUTH);
// *************************************************
// 右上角的X-关闭按钮-添加事件处理
addWindowListener(new WindowAdapter() {
// 适配器
@Override
public void windowClosing(WindowEvent e) {
if (pw == null) {
System.exit(0);
}
String msg = "exit@#@#全部@#@#null@#@#" + tfdUserName.getText();
pw.println(msg);
pw.flush();
System.exit(0);
}
});
setBounds(300, 300, 400, 300);
setVisible(true);
}
private void addJMenu() {
JMenuBar menuBar = new JMenuBar();
this.setJMenuBar(menuBar);
JMenu menu = new JMenu("选项");
menuBar.add(menu);
JMenuItem menuItemSet = new JMenuItem("设置");
JMenuItem menuItemHelp = new JMenuItem("帮助");
menu.add(menuItemSet);
menu.add(menuItemHelp);
menuItemSet.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final JDialog dlg = new JDialog(ClientForm.this);// 弹出一个界面
// 不能直接用this
dlg.setBounds(ClientForm.this.getX()+20, ClientForm.this.getY()+30,
350, 150);
dlg.setLayout(new FlowLayout());
dlg.add(new JLabel("服务器IP和端口:"));
final JTextField tfdHost = new JTextField(10);
tfdHost.setText(ClientForm.HOST);
dlg.add(tfdHost);
dlg.add(new JLabel(":"));
final JTextField tfdPort = new JTextField(5);
tfdPort.setText(""+ClientForm.PORT);
dlg.add(tfdPort);
JButton btnSet = new JButton("设置");
dlg.add(btnSet);
btnSet.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String ip = tfdHost.getText();//解析并判断ip是否合法
String strs[] = ip.split("\\\\.");
if(strs==null||strs.length!=4){
JOptionPane.showMessageDialog(ClientForm.this, "IP类型有误!");
return ;
}
try {
for(int i=0;i<4;i++){
int num = Integer.parseInt(strs[i]);
if(num>255||num<0){
JOptionPane.showMessageDialog(ClientForm.this, "IP类型有误!");
return ;
}
}
} catch (NumberFormatException e2) {
JOptionPane.showMessageDialog(ClientForm.this, "IP类型有误!");
return ;
}
ClientForm.HOST=tfdHost.getText();//先解析并判断ip是否合法
try {
int port = Integer.parseInt( tfdPort.getText() );
if(port<0||port>65535){
JOptionPane.showMessageDialog(ClientForm.this, "端口范围有误!");
return ;
}
} catch (NumberFormatException e1) {
JOptionPane.showMessageDialog(ClientForm.this, "端口类型有误!");
return ;
}
ClientForm.PORT=Integer.parseInt( tfdPort.getText() );
dlg.dispose();//关闭这个界面
}
});
dlg.setVisible(true);//显示出来
}
});
menuItemHelp.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JDialog dlg = new JDialog(ClientForm.this);
dlg.setBounds(ClientForm.this.getX()+30,ClientForm.this.getY()+30, 400, 100);
dlg.setLayout(new FlowLayout());
dlg.add(new JLabel("版本所有@陈浩翔.2016.5.16 我的主页:http://chenhaoxiang.github.io"));
dlg.setVisible(true);
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("c")) {
if (tfdUserName.getText() == null
|| tfdUserName.getText().trim().length() == 0
|| "@#@#".equals(tfdUserName.getText())
|| "@#".equals(tfdUserName.getText())) {
JOptionPane.showMessageDialog(this, "用户名输入有误,请重新输入!");
return;
}
connecting();// 连接服务器的动作
if (pw == null) {
JOptionPane.showMessageDialog(this, "服务器未开启或网络未连接,无法连接!");
return;
}
((JButton) (e.getSource())).setEnabled(false);
// 获得btnCon按钮--获得源
// 相当于btnCon.setEnabled(false);
btnExit.setEnabled(true);
btnSend.setEnabled(true);
tfdUserName.setEditable(false);
} else if (e.getActionCommand().equals("send")) {
if (tfdMsg.getText() == null
|| tfdMsg.getText().trim().length() == 0) {
return;
}
String msg = "on@#@#" + list.getSelectedValue() + "@#@#"
+ tfdMsg.getText() + "@#@#" + tfdUserName.getText();
pw.println(msg);
pw.flush();
// 将发送消息的文本设为空
tfdMsg.setText("");
} else if (e.getActionCommand().equals("exit")) {
//先把自己在线的菜单清空
lm.removeAllElements();
sendExitMsg();
btnCon.setEnabled(true);
btnExit.setEnabled(false);
tfdUserName.setEditable(true);
}
}
// 向服务器发送退出消息
private void sendExitMsg() {
String msg = "exit@#@#全部@#@#null@#@#" + tfdUserName.getText();
System.out.println("退出:" + msg);
pw.println(msg);
pw.flush();
}
private void connecting() {
try {
// 先根据用户名防范
String userName = tfdUserName.getText();
if (userName == null || userName.trim().length() == 0) {
JOptionPane.showMessageDialog(this, "连接服务器失败!\\r\\n用户名有误,请重新输入!");
return;
}
clientSocket = new Socket(HOST, PORT);// 跟服务器握手
pw = new PrintWriter(clientSocket.getOutputStream(), true);// 加上自动刷新
pw.println(userName);// 向服务器报上自己的用户名
this.setTitle("用户[ " + userName + " ]上线...");
new ClientThread().start();// 接受服务器发来的消息---一直开着的
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
class ClientThread extends Thread {
@Override
public void run() {
try {
Scanner sc = new Scanner(clientSocket.getInputStream());
while (sc.hasNextLine()) {
String str = sc.nextLine();
String msgs[] = str.split("@#@#");
System.out.println(tfdUserName.getText() + ": " + str);
if ("msg".equals(msgs[0])) {
if ("server".equals(msgs[1])) {// 服务器发送的官方消息
str = "[ 通知 ]:" + msgs[2];
} else {// 服务器转发的聊天消息
str = "[ " + msgs[1] + " ]说: " + msgs[2];
}
allMsg.append("\\r\\n" + str);
}
if ("cmdAdd".equals(msgs[0])) {
boolean eq = false;
for (int i = 0; i < lm.getSize(); i++) {
if (lm.getElementAt(i).equals(msgs[2])) {
eq = true;
}
}
if (!eq) {
lm.addElement(msgs[2]);// 用户上线--添加
}
}
if ("cmdRed".equals(msgs[0])) {
lm.removeElement(msgs[2]);// 用户离线了--移除
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);// 设置装饰
new ClientForm();
}
}
以上是关于Java---基于TCP协议的相互即时通讯小程序的主要内容,如果未能解决你的问题,请参考以下文章