模拟QQ,在线群聊:底层就是 采用 TCP的编程思想,每个用户必须连接到服务器才能进行聊天,用户之间的群聊还是私聊都必须要用过服务器进行处理和转发。
网络通信的关键就是在于协议,所以设计软件最麻烦的就是在定义协议这个地方,需要统一信息传递的格式。
协议如下:
客户端向服务器发的消息格式设计:
命令关键字@#接收方@#发送方@#消息内容
1)连接:userName ----握手的线程serverSocket专门接收该消息,其它的由服务器新开的与客户进行通讯的socket来接收
2)退出:exit@#全部@#userName@#null
3)发送: on @# list.getSelectedValue() @# tfdUserName.getText() @# tfdMsg.getText()
服务器向客户端发的消息格式设计:
命令关键字@#发送方@#消息内容
登录:
cmdAdd@#server @# userName (给客户端维护在线用户列表用的)
退出:
cmdRed@#server @# userName (给客户端维护在线用户列表用的)
发送:
msg @#消息发送者 @# 消息内容
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.border.TitledBorder;
/**
*
* 2018年5月13日 上午8:55:32
* @author 宋进宇
*
*/
public class ClientForm extends JFrame implements ActionListener{
private static final long serialVersionUID = 1L;
//服务器端的 IP 和 PORT
private static final String IP = "127.0.0.1";
private static final int PORT = 9999;
private JTextField tfdUserName; //用户标识
private JList list; //在线用户列表--表现层
private DefaultListModel lm; //在线用户列表--数据层
private JTextArea allMsg = new JTextArea(); //消息显示主窗口
private JTextField tfdMsg; //发消息输入框
private Socket socket = null;
private JButton btnConn;
public ClientForm() {
setBounds( 300, 300, 400, 300 );
//1上部面板
JPanel p = new JPanel();
p.add( new JLabel( "用户标识:" ) );
tfdUserName = new JTextField( 10 );
p.add( tfdUserName );
btnConn = new JButton( "连接" );
btnConn.setActionCommand( "connection" );
btnConn.addActionListener( this );
p.add( btnConn );
JButton btnExit = new JButton( "退出" );
btnExit.setActionCommand( "exit" );
btnExit.addActionListener( this );
p.add( btnExit );
this.add( p, BorderLayout.NORTH ); //添加到上部
//2中部面板
JPanel centerP = new JPanel();
centerP.setLayout( new BorderLayout() );
//2.1东 在线用户列表
lm = new DefaultListModel();
lm.addElement( "全部" );
list = new JList( lm );
list.setSelectedIndex( 0 );
list.setVisibleRowCount( 2 );
JScrollPane jsc = new JScrollPane( list );
jsc.setBorder( new TitledBorder( "在线" ) );
jsc.setPreferredSize( new Dimension( 70, centerP.getHeight() ) );
centerP.add( jsc, BorderLayout.EAST );
//2.2中 聊天信息窗口
allMsg.setEditable( false );
centerP.add( new JScrollPane( allMsg ) );
//2.3南 消息发送面板
JPanel sendP=new JPanel();
sendP.add( new JLabel( "消息:" ) );
tfdMsg = new JTextField( 20 );
sendP.add( tfdMsg );
JButton btnSend = new JButton( "发送" );
btnSend.setActionCommand( "send" );
btnSend.addActionListener( this );
sendP.add( btnSend );
centerP.add( sendP, BorderLayout.SOUTH );
this.add( centerP ); //添加到中部
addWindowListener( new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
//退出前,先服务器发送退出消息
sendExitMsg();
}
});
setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
if ( "connection".equals( e.getActionCommand() ) ) {//链接
try {
socket = new Socket( IP, PORT );
//连接成功就给服务器发送用户名
PrintWriter pw = new PrintWriter( socket.getOutputStream(), true );
pw.println( tfdUserName.getText() );
//把 tfdUserName 设置不可编辑,同时 把 连接按钮置灰
tfdUserName.setEditable( false );
btnConn.setEnabled( false );
//同时设置一下 标题
setTitle( "Java修仙群-在线中..." );
//开一个线程接收服务器发送过来的信息
new Thread( new Receive() ).start();;
} catch (IOException e1) {
JOptionPane.showMessageDialog( this, "服务器未启动..." );
return;
}
} else if ( "exit".equals( e.getActionCommand() ) ) {
//退出前向服务器发送退出消息
sendExitMsg();
} else if ( "send".equals( e.getActionCommand() ) ) {
//如果 用户没有登入 就不能发送信息
if ( socket == null ) {
JOptionPane.showMessageDialog( this, "请您登入后再发送信息");
return;
}
PrintWriter pw = null ;
try {
pw = new PrintWriter( socket.getOutputStream(), true );
} catch (IOException e2) {
e2.printStackTrace();
return;
}
int index = list.getSelectedIndex();
if ( index == 0 ) { //如果index==0说明是发送给所有人
String mes = "send@#all@#" + tfdUserName.getText() + "@#" + tfdMsg.getText();
pw.println( mes );
//更新消息栏
allMsg.append( "您说:" + tfdMsg.getText() + "\r\n" );
} else {//否则 就是 私发消息
if ( lm.get(index).equals( tfdUserName.getText() ) ) {
JOptionPane.showMessageDialog( this, "不能跟自己私聊..." );
return;
}
String mes = "send@#" + lm.get(index) + "@#" + tfdUserName.getText() + "@#" + tfdMsg.getText();
pw.println( mes );
allMsg.append( "您悄悄对" + lm.get(index) + "说:" + tfdMsg.getText() + "\r\n" );
}
//发送完毕后清空tfdMsg
tfdMsg.setText( "" );
}
}
/**
* 发送退出消息
*/
private void sendExitMsg() {
if ( socket != null ) {
try {
PrintWriter pw = new PrintWriter( socket.getOutputStream(), true );
pw.println( "exit@#all@#" + tfdUserName.getText() + "@#" );
} catch (IOException e1) {
e1.printStackTrace();
return;
} finally {
//退出程序
System.exit( 0 );
}
} else {
System.exit( 0 );
}
}
/**
* 2018年5月12日 下午8:09:11
* @author 宋进宇
* 内部类,用来接收 别的用户发送过来的信息
*/
class Receive implements Runnable {
@Override
public void run() {
//如果 socket 就退出
if ( socket == null ) {
return;
}
BufferedReader br = null;
try {
br = new BufferedReader(
new InputStreamReader( socket.getInputStream() ) );
//只有有消息就接收
while ( true ) {
String mes = br.readLine();
//如果 消息无效 就跳过
if ( mes == null || mes.trim().length() == 0) {
continue;
}
dealWithMessage( mes );
}
} catch (IOException e) {
e.printStackTrace();
return;
}
}
/**
* 处理服务器发送过来的信息
* 信息内容格式 --命令关键字@#发送方@#消息内容
* 命令关键字:cmdAdd,cmdRem,msg
* 发送方: server/otherUserNaem
* 消息内容:userNaem/消息内容
* @param mes 服务器发送过来的信息
*/
private void dealWithMessage(String mes) {
String[] strs = mes.split( "@#" );
if ( strs.length < 3 ) {
return;
}
if ( "cmdAdd".equals( strs[0] ) ) { //处理cmdAdd指令
//更新 allMsg
allMsg.append( "通知:用户[" + strs[2] + "]上线了\r\n" );
//更新 lm
lm.addElement( strs[2] ); //更新数据
list.validate(); //实时更新表现层
} else if ( "cmdRem".equals( strs[0] ) ) { //处理cmdRem指令
//更新 allMsg
allMsg.append( "通知:用户[" + strs[2] + "]下线了\r\n" );
//更新 lm
lm.removeElement( strs[2] ); //更新数据
list.validate(); //实时更新表现层
} else if ( "msg".equals( strs[0] ) ) { //处理msg指令
if ( "server".equals( strs[1] ) ) { //如果是服务器发送的信息说明是用来初始化的在线用户列表的信息
allMsg.append( "欢迎您登入!\r\n" );
for (int i = 2; i < strs.length; i++) {
//更新在线用户列表
lm.addElement( strs[i] ); //更新数据
list.validate(); //实时更新表现层
}
}else {//否则的话,就是用户之间的通信了
allMsg.append( strs[1] + mes.substring( strs[0].length() + strs[1].length() + 4 ) + "\r\n" );
}
}
}
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
new ClientForm();
}
}
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.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
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.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.border.TitledBorder;
/**
* 2018年5月13日 上午7:39:54
* @author 宋进宇
*
*/
public class ServerForm extends JFrame {
private static final long serialVersionUID = 1L;
//服务器的端口
private static int PORT = 9999;
private JList list; //在线用户列表--表现层
private DefaultListModel lm; //在线用户列表--数据层
private JTextArea allInfo = new JTextArea(); //信息显示主窗口
private Map userSockets = new HashMap();
private JMenuItem itemRun;
public ServerForm() {
setTitle( "啊哈哈-服务器" );
setDefaultCloseOperation(EXIT_ON_CLOSE);
//1东 在线用户列表
lm = new DefaultListModel();
lm.addElement( "全部" );
list = new JList( lm );
list.setVisibleRowCount( 5 );
JScrollPane jsc = new JScrollPane( list );
jsc.setBorder( new TitledBorder( "在线" ) );
jsc.setPreferredSize( new Dimension( 100, this.getHeight() ) );
getContentPane().add( jsc, BorderLayout.EAST );
//2中 信息主窗口
allInfo.setEditable( false );
getContentPane().add( new JScrollPane( allInfo ) );
//菜单
JMenuBar menubar = new JMenuBar();
setJMenuBar( menubar );
JMenu menu = new JMenu( "控制(C)" );
menubar.add( menu );
menu.setMnemonic( 'C' ); //设置菜单助记符
itemRun = new JMenuItem( "开启" );
itemRun.setActionCommand( "run" );
itemRun.setAccelerator(KeyStroke.getKeyStroke( 'R', KeyEvent.CTRL_MASK ) );//设置快捷键: Ctrl+R
menu.add( itemRun );
menu.addSeparator();
JMenuItem itemExit = new JMenuItem( "退出" );
itemExit.setActionCommand( "exit" );
itemExit.setAccelerator( KeyStroke.getKeyStroke( 'E', KeyEvent.CTRL_MASK ) );//设置快捷键: Ctrl+E
menu.add( itemExit );
//监听器
ActionListener al = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if( e.getActionCommand().equals( "run" ) ){
new Thread() {//采用 匿名内部类,重写Thread的 run方法
public void run() {
serverRun();
}
}.start();
}else if( e.getActionCommand().equals( "exit" ) ){
System.exit( 0 );
}
}
};
//给2个菜单项添加监听
itemRun.addActionListener( al );
itemExit.addActionListener( al );
int winWidth = 500;
int winHeight = 400;
Toolkit toolkit = Toolkit.getDefaultToolkit();
int width = (int) toolkit.getScreenSize().getWidth();
int height = (int)toolkit.getScreenSize().getHeight();
setBounds( width/2 - winWidth/2, height/2 - winHeight/2, winWidth, winHeight );
setVisible(true);
}
/**
* 启动服务器
*/
protected void serverRun() {
ServerSocket server = null;
try {
server = new ServerSocket( PORT );
//在 服务器信息栏 添加服务器启动信息
allInfo.append( server + "--服务器启动了\r\n" );
//启动服务器后,关闭启动菜单项
itemRun.setEnabled( false );
while ( true ) {
Socket s = server.accept();
new Thread( new UserRun( s ) ).start();
}
} catch (IOException e) {
System.exit( -1 );
} finally {
if ( server != null ) {
try {
server.close();
} catch (IOException e) {
}
}
}
}
class UserRun implements Runnable {
private Socket s = null;
public UserRun(Socket s) {
this.s = s;
}
@Override
public void run() {
//用户一连接上就获取用户IP
String userIp = s.getInetAddress().getHostAddress();
BufferedReader br = null;
try {
//能到这里说明一个用户链接成功
//获取用户名
//这里规定用户名为一行
br = new BufferedReader(
new InputStreamReader( s.getInputStream() ) );
String userName = br.readLine();
//把用户信息放人 userSockets 中
userSockets.put( userName, s );
//更新allInfo
allInfo.append( "IP:" + userIp + ",port:" + s.getPort() + ",userName:" + userName + ",上线了!\r\n");
lm.addElement( userName );
//把在线的用户信息反馈给当前用户
initData();
//同时通知其他用户
String mes = "cmdAdd@#server@#" + userName;
notifyOthersUser( mes, userName );
//通知完毕后等待用户发来信息
while ( true ) {
//用户信息
mes = br.readLine();
//如果 mes 为无效数据就跳过
if ( mes == null || mes.trim().length() == 0) {
continue;
}
//解析用户信息
//用户信息规则:命令关键字@#接收方@#发送方@#消息内容
//命令:exit/send,接收方:all/otherUserName,发送方:userName,消息内容:...
try {
dealWithMessage( mes );
} catch (Exception e) {
//用户下线就退出循环
break;
}
}
} catch (IOException e) {
}
}
/**
* 给用户反馈当前 在线 用户。
*/
private void initData() {
try {
PrintWriter pw = new PrintWriter( s.getOutputStream(), true );
StringBuilder sb = new StringBuilder( "msg@#server" );
Set names = userSockets.keySet();
for (String name : names) {
sb.append( "@#" + name );
}
pw.println( sb.toString() );
} catch (IOException e) {
e.printStackTrace();
return ;
}
}
/**
* 处理用户发送的信息,
* 用户信息规则:命令关键字@#接收方@#发送方@#消息内容,
* 命令:exit/send,
* 接收方:all/otherUserName,
* 发送方:userName,
* 消息内容:...
* @param mes 用户发送的信息
* @throws Exception 抛出该异常说明用户下线了
*/
private void dealWithMessage( String mes ) throws Exception {
//如果消息无效 就不处理
if ( mes == null || mes.trim().length() == 0) {
return;
}
String[] strs = mes.split("@#");
if ( "exit".equals( strs[0] ) ) {//进行退出处理
String str = "cmdRem@#server@#" + strs[2] ;
notifyOthersUser( str, strs[2] );
//同时在 userSockets中移除该用户
Socket s = userSockets.remove( strs[2] );
//更新allInfo
allInfo.append( "IP:" + s.getInetAddress().getHostAddress() + ",port:" + s.getPort() + ",userName:" + strs[2] + ",下线了!\r\n");
lm.removeElement( strs[2] );
//关闭s
s.close();
throw new Exception();
} else if ( "send".equals( strs[0] ) ){//进行发送处理
//组织信息
String str = "msg@#" + strs[2] + "@#说:" + mes.substring( strs[0].length() + strs[1].length() + strs[2].length() + 6 );
if ( "all".equals( strs[1] ) ) {//如果是发送给所有人
System.out.println( str );
notifyOthersUser( str, strs[2] );
} else {
str = "msg@#" + strs[2] + "@#悄悄对您说:" + mes.substring( strs[0].length() + strs[1].length() + strs[2].length() + 6 );
Socket target = userSockets.get( strs[1] );
System.out.println( strs[1] );
PrintWriter pw = new PrintWriter( target.getOutputStream(), true );
pw.println( str );
}
}
}
/**
* 通知除了 userName 的其他用户。通知内容格式 --命令关键字@#发送方@#消息内容
* 命令关键字:cmdAdd,cmdRem,msg
* 发送方: server/otherUserNaem
* 消息内容:userNaem/消息内容
* @param mes 通知内容格式 --命令关键字@#发送方@#消息内容
* @param userName 该用户状态
*/
private void notifyOthersUser(String mes, String userName) {
Set> entrys = userSockets.entrySet();
for (Entry en : entrys) {
//如果是当前用户就跳过
if ( en.getKey().equals( userName ) ){
continue;
}
Socket s = en.getValue();
try {
PrintWriter out = new PrintWriter( s.getOutputStream(), true );
//给 除了 userName 的其他所有用户发送消息
out.println( mes );
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
new ServerForm();
}
}