自学JAVA,学到网络通信socket,很困惑,想写一个客户端对客户端的简单例子,但是网上一搜,全都是客户端对服务端,很无奈,百度提问一个月没回答,因此自己寻找各种途径,终于写成。代码如下,其中必然有很多代码冗余和bug,例子并不规范,只是实现了核心功能。希望对有同样困惑的同学有所帮助启发。
不过代码中有一些问题我自己还没思考到,但是不影响本帖子核心功能点的实现。其次,有些注释代码是写代码的过程中出现的错误,我保留了痕迹没删除,只是注释掉了。
首先需要几个工具类:
1,窗体拖动功能类,因为JFrame和JDialog去除边框后不能拖动。代码如下
package nio.file;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
/*
window drag 工具类
*/
class WindowDrag
{
private static Point point;
//匿名内部类中访问局部变量,这个局部变量一定要用final修饰
public static void allowDrag(final Window window)
{
window.addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent e)
{
//获取鼠标按下时的坐标
point=e.getPoint();
}
});
//注册鼠标拖动监听器
window.addMouseMotionListener(new MouseMotionAdapter()
{
//鼠标拖动方法
public void mouseDragged(MouseEvent e)
{
//获取窗体的坐标点
Point win_old=window.getLocation();
//获取鼠标的时时坐标点
Point mouse_new=e.getPoint();
//鼠标旧坐标x y值
double mouse_x_old=point.getX();
double mouse_y_old=point.getY();
//鼠标新坐标x y值
double mouse_x_new=mouse_new.getX();
double mouse_y_new=mouse_new.getY();
//鼠标坐标x y增长值
double mouse_x_increment=mouse_x_new - mouse_x_old;
double mouse_y_increment=mouse_y_new - mouse_y_old;
//窗体旧坐标x y值
double win_x_old=win_old.getX();
double win_y_old=win_old.getY();
//窗体新坐标x y值(旧值加上增量)
//窗体用setLocation方法设置坐标的时候要用int,因此需要强转
int win_x_new=(int)(win_x_old + mouse_x_increment);
int win_y_new=(int)(win_y_old + mouse_y_increment);
//设置窗体坐标值
window.setLocation(win_x_new,win_y_new);
}
});
}
}
2,消息类型参数类,存放的就是一堆字符串常量静态字段
package nio.file;
/*
参数常量
存储消息类的字段
该类存储的都是消息类的常量字符串字段
*/
class MessageConstants
{
/**发出消息方:服务端*/
public static String SERVER="SERVER";
/**发出消息方:客户端*/
public static String CLIENT="CLIENT";
/**消息类型:请注册消息*/
public static String REGISTER="REGISTER";
/**消息类型:注册成功消息*/
public static String REGISTER_SUCCESS="REGISTER_SUCCESS";
/**消息类型:欢迎消息*/
public static String WELCOME="WELCOME";
/**消息类型:申请会话*/
public static String APPLY="APPLY";
/**消息类型:申请成功*/
public static String APPLY_SUCCESS="APPLY_SUCCESS";
/**消息类型:申请失败*/
public static String APPLY_FIELD="APPLY_FIELD";
/**消息类型:正常消息*/
public static String NORMAL="NORMAL";
}
3,图片对象获取类,这个是为了简单优化界面用的,获取图片ImageIcon类对象,设置一些组件的背景,icon什么的,代码如下
package nio.file;
import javax.swing.ImageIcon;
/*
图片获取工具
*/
class PictureUtil
{
public static ImageIcon getImageIcon(String pic_name)
{
return new ImageIcon(PictureUtil.class.getClassLoader().getResource("nio/file/image/"+pic_name));
}
}
4,消息类,这个很重要,不管是客户端还是服务端,在发送消息的时候,消息必须具备特定的格式,要不然收到消息方很难判断这个消息类型,或者消息的发送者是谁。消息类要包含几个必须的字段,比如发送者nick,消息类型style(消息类型字段就存放在上面2中的消息类型工具类中),比如是验证消息,申请消息,注册消息,等等,还有消息内容,content等,喜欢的话还可以加上发送消息时间等。消息类其实就是一个JavaBean,代码如下:
package nio.file;
/*
消息类,存放消息的各种参数
1,发送者/接受者ip,端口,时间,以及发送者身份,如客户端,服务端
2,发送消息类型,以及消息内容
*/
class Message
{
/**发送者ip*/
private String sender_ip;
/**发送者端口*/
private int sender_port;
/**发送时间*/
private String send_date;
/**发送者昵称*/
private String sender_nick;
/**接收方IP*/
private String receive_ip;
/**接收方端口*/
private int receive_port;
/**聊天对方nick*/
private String talk_nick;
/**消息类型*/
private String style;
/**消息内容*/
private String content;
public String getSender_ip() {
return sender_ip;
}
public void setSender_ip(String sender_ip) {
this.sender_ip = sender_ip;
}
public int getSender_port() {
return sender_port;
}
public void setSender_port(int sender_port) {
this.sender_port = sender_port;
}
public String getSender_nick()
{
return sender_nick;
}
public void setSender_nick(String sender_nick)
{
this.sender_nick=sender_nick;
}
public String getTalk_nick()
{
return talk_nick;
}
public void setTalk_nick(String talk_nick)
{
this.talk_nick=talk_nick;
}
public String getSend_date() {
return send_date;
}
public void setSend_date(String send_date) {
this.send_date = send_date;
}
public String getReceive_ip() {
return receive_ip;
}
public void setReceive_ip(String receive_ip) {
this.receive_ip = receive_ip;
}
public int getReceive_port() {
return receive_port;
}
public void setReceive_port(int receive_port) {
this.receive_port = receive_port;
}
public String getStyle() {
return style;
}
public void setStyle(String style) {
this.style = style;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
/**构造函数*/
public Message(String style,String content)
{
this.style=style;
this.content=content;
}
public Message(String content)
{
this.content=content;
}
}
5,将消息类转换成Json字符串的工具类JsonUtil,为什么要转换?因为不管是服务器还是客户端,在通过通道channel发送消息的时候发送的都是用缓冲区发送的字节,4步骤中的消息类包含的信息是要发送的内容,肯定不能直接把消息对象发出去,必须把消息类对象转成字符串,但是转化后的字符串必须符合一定的规范,要么是XML,要么是JSon,Json更常用,这里需要导入一个第三方jar包,叫:gson-1.6.jar,网上搜索随便下载,很多。代码如下
package nio.file;
import com.google.gson.Gson;
/*
Gson工具类
功能:
1,将对象转换成Json字符串
2,将字符串转成对应的类对象
*/
class JsonUtil
{
//将类对象转成Json字符串
public static String toJson(Message msg)
{
Gson gson=new Gson();
String content=gson.toJson(msg);
return content;
}
//将字符串转成指定的类对象
public static Message toBean(String content)
{
Gson gson=new Gson();
Message msg= gson.fromJson(content,Message.class);
return msg;
}
}
6,服务端,其实客户端对客户端,就是通过服务端的消息转发,比如A要发消息给B,AB首先都需要链接服务器,然后注册自己的NICK,服务器吧个字的NICK和socketAddress全存入一个map集合,然后再把socketAddress和对应的socketChannel存入一个map集合,A要发消息给B,那么A发送给服务器的消息类中就必须包含聊天对象B的nick,服务器根据B的nick找到B的通道,然后将消息转发给B的通道即可。这就是逻辑,服务器代码如下(服务器代码有很多中写法,可以用线程,我这个不是标准,只是实现)
package nio.file;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Set;
/*
基于NIO的服务端
*/
class ServerByNio
{
/**通道管理器*/
private Selector selector;
/**服务器通道*/
private ServerSocketChannel serverSocketChannel;
/**为服务器分配的ip*/
private String host="localhost";
/**为服务器程序分配的端口*/
private int PORT=8888;
/**客户端通道*/
/**存储发送消息者昵称和发送方ip和端口对象的map集合*/
private HashMap<String,SocketAddress> nick_Address_map;
/**存储发送方地址对象,和发送方通道对象的集合*/
private HashMap<SocketAddress,SocketChannel> address_SocketChannel_map;
//构造函数,初始化各个参数
ServerByNio() throws IOException
{
//初始两个map集合
nick_Address_map=new HashMap<String,SocketAddress>();
address_SocketChannel_map=new HashMap<SocketAddress,SocketChannel>();
//初始化选择器
selector=Selector.open();
//初始化服务器通道
serverSocketChannel=ServerSocketChannel.open();//至此,服务器已经启动
System.out.println("服务器已经启动!");
//服务器通道绑定ip和端口
serverSocketChannel.socket().bind(new InetSocketAddress(host,PORT));
//设置通道非阻塞
serverSocketChannel.configureBlocking(false);
//将通道与通道管理器绑定,并且注册通道感兴趣的事件,通过register方法实现
/**注释:服务器通道感兴趣的事件一般就是接收事件*/
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
}
//通道管理器轮询监听其管理(即与之绑定)的通道是否有处于就绪状态的
public void listen()
{
while(true)
{
try
{
selector.select();//该方法阻塞,返回的是就绪的通道的个数
//如果程序接着执行,代表有通道就绪,即有事件发生,此时,选择器通过selectedKeys方法返回触发的选择键
//选择键,每触发一个事件,代表一个选择键被选择
System.out.println("客户端链接成功!!");
Set<SelectionKey> keys=selector.selectedKeys();
for(SelectionKey key:keys)
{
keys.remove(key);
if(!key.isValid())
continue;
handelKey(key);
}
}
catch(IOException e)
{
System.out.println("跑了异常了");
e.printStackTrace();
}
}
}
public void handelKey(SelectionKey key) throws IOException
{
//客户端通道对象
SocketChannel socketChannel=null;
//判断key的类型
if(key.isValid() && key.isAcceptable())
{
//客户端通道通过accept方法接收到申请链接的客户端通道
socketChannel=serverSocketChannel.accept();
//至此,客户端链接服务器成功
System.out.println("客户端链接服务器成功!");
//设置客户端通道非阻塞
socketChannel.configureBlocking(false);
//将这个通道与服务端通道管理器绑定,并且注册感兴趣的事件
socketChannel.register(selector,SelectionKey.OP_READ);
//欢迎消息
doWelcome(socketChannel);
//将客户端通道的原地址,和客户端通道存入map集合
address_SocketChannel_map.put(socketChannel.socket().getRemoteSocketAddress(),socketChannel);
System.out.println(socketChannel.socket().getRemoteSocketAddress().toString());
System.out.println(address_SocketChannel_map);
}
if(key.isValid()&& key.isReadable())
{
//获取触发key的通道
socketChannel=(SocketChannel)key.channel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
//定义存储读取到的数据的String
String content="";
buffer.clear();
//通过socketChannel的read方法把数据读取入缓冲区
int count=socketChannel.read(buffer);
if(count>0)
{
//count>0代表读取到数据
buffer.flip();
byte[] data=new byte[buffer.remaining()];
buffer.get(data);//用缓冲区的数据元素依次填充date数组
content+=new String(data);
//处理读取到的数据
doReadData(socketChannel,content);
}
//难道这句话的执行逻辑是这样的:将key设置为下一次可读,没读完继续读?
//如果不是,上面的不是应该用while循环吗?if只是读取依次,万一没读完,咋办?
key.interestOps(SelectionKey.OP_READ);
if(count==-1)
{
// System.out.println("count="+count);
//count==-1代表客户端已经关闭,那这个键就没意义了,那么就取消改建,关闭客户端通道
key.cancel();//这句话的实际意思是,取消触发该键的通道到选择器的绑定注册
System.out.println("count="+count);
try
{
if(socketChannel!=null)
{
socketChannel.close();
System.out.println("关闭客户端通道");
}
}
catch(IOException e)
{
e.printStackTrace();
}
}
//处理读取到的数据
// doReadData(socketChannel,content);
}
}
public void doWelcome(SocketChannel socketChannel) throws IOException
{
//链接服务器成功,服务器要求客户端输入昵称信息
String wel_msg="连接服务器成功,请注册您的昵称:";
Message msg=new Message(wel_msg);
msg.setStyle(MessageConstants.REGISTER);
String msg_json=JsonUtil.toJson(msg);
socketChannel.write(ByteBuffer.wrap(msg_json.getBytes()));
System.out.println("服务端欢迎消息执行完成");
}
public void doReadData(SocketChannel socketChannel,String content) throws IOException
{
Message msg=JsonUtil.toBean(content);
if(MessageConstants.REGISTER.equals(msg.getStyle()))
{
//获取消息发送方的nick
String nick=msg.getSender_nick();
//这个地方要注意,getLocalSocketAddress()返回的是该通道申请链接的服务器的socketAddress,即使申请连接的服务器的ip和端口
//因为开启的客户端申请连接的服务器都一样,因此通过该方法获取的socketAddress对象都一样
//这里存储的必须是客户端通道自身的socketAddress,必须是自己的ip和端口,在同一台主机上,开启多个客户端的时候,ip虽然一样,但是随机分配的端口是不一样的,获取的socketAddress也不一样,因此
//可以在同一台主机上启动多个端口互发消息而不会产生错发或者错收消息的情况。一台主机开多个qq道理一样。
nick_Address_map.put(nick,socketChannel.socket().getRemoteSocketAddress());
System.out.println(nick+":"+nick_Address_map.get(nick).toString());
System.out.println(nick_Address_map);
// System.out.println(nick+":"+socketChannel.socket().getRemoteSocketAddress().toString());
// System.out.println("服务端存放nick和地址成功");
doRegistSuc(socketChannel,nick);
}
if(MessageConstants.APPLY.equals(msg.getStyle()))
{
//获取申请聊天对象的nick
String talk_nick=msg.getTalk_nick();
//获取nick和地址map集合的key值set集合,判断这个talk_nick是否存在.
// Set<String>keys=nick_Address_map.keySet();
boolean flag=judgeKey(talk_nick,nick_Address_map);
if(flag)
{
String text=msg.getSender_nick()+",您好,您申请与"+talk_nick+"会话连接成功,请开始会话!";
Message msg1=new Message(MessageConstants.APPLY_SUCCESS,text);
String msg_json=JsonUtil.toJson(msg1);
socketChannel.write(ByteBuffer.wrap(msg_json.getBytes()));
}
else
{
String text=msg.getSender_nick()+",您好,您申请与"+talk_nick+"会话连接失败,因为不存在这个id,请检查!";
Message msg2=new Message(MessageConstants.APPLY_FIELD,text);
String msg_json=JsonUtil.toJson(msg2);
socketChannel.write(ByteBuffer.wrap(msg_json.getBytes()));
}
}
if(MessageConstants.NORMAL.equals(msg.getStyle()))
{
//正常的通信,服务器找到nick对应的客户端通道,进行消息转发
String nick=msg.getTalk_nick();
System.out.println(nick);
SocketChannel socketChannel1=address_SocketChannel_map.get(nick_Address_map.get(nick));
//将需要转发的消息写入对应的通道,需要转发的消息就是刚刚读取到的消息content
socketChannel1.write(ByteBuffer.wrap(content.getBytes()));
}
}
public boolean judgeKey(String key,HashMap<String,SocketAddress> map)
{
Set<String>keys=map.keySet();
for(String k:keys)
{
if(key.equals(k))
return true;
}
return false;
}
public void doRegistSuc(SocketChannel socketChannel,String nick) throws IOException
{
//注册成功后,服务器返回消息:nick+注册成功,欢迎开启聊天!
String content=nick+"您好,恭喜您注册成功,欢迎开启聊天!";
Message msg=new Message(MessageConstants.REGISTER_SUCCESS,content);
String msg_json=JsonUtil.toJson(msg);
socketChannel.write(ByteBuffer.wrap(msg_json.getBytes()));
}
public void close()
{
if(serverSocketChannel!=null)
{
try
{
serverSocketChannel.close();
}
catch(IOException e)
{
e.printStackTrace();
}
try
{
selector.close();
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException
{
new ServerByNio().listen();
}
}
7,客户端,用swing简单的界面实现
package nio.file;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.border.TitledBorder;
/*
客户端
*/
class ClientByNio extends JDialog implements ActionListener
{
/**
*
*/
private static final long serialVersionUID = 1L;
/**客户端通道管理器*/
private Selector selector;
/**客户端通道*/
private SocketChannel socketChannel;
/**申请链接服务器的IP*/
private String host="localhost";
/**申请链接服务器端口*/
private int PORT=8888;
/**主面板*/
private JPanel contentPane;
/**顶部面板*/
private JPanel top_pane;
/**我是文本框*/
private JTextField my_nick;
/**注册按钮*/
private JButton btn_register;
/**对方名字文本框*/
private JTextField talk_nick;
/**开启聊天按钮*/
private JButton talk_btn;
/**关闭标签*/
private JLabel exit_btn;
/**中部消息文本区域*/
private JTextArea show_msg_area;
/**底部面板*/
private JPanel bottom_pane;
/**发送消息文本区域*/
private JTextArea send_msg_area;
/**清空消息面板按钮*/
private JButton clear_btn;
/**取消发送消息按钮*/
private JButton cancel_btn;
/**发送消息按钮*/
private JButton send_btn;
/**关闭按钮*/
private JButton close_btn;
//构造函数,初始化各字段
ClientByNio() throws IOException
{
//初始化界面
initGui();
//注册监听器
initLisenter();
//通道初始化
initChannel();
}
public void initChannel() throws IOException
{
selector=Selector.open();
//开启客户端通道
socketChannel=SocketChannel.open(new InetSocketAddress(host,PORT));
//设置通道非阻塞
socketChannel.configureBlocking(false);
//与通道管理器绑定并注册感兴趣事件
socketChannel.register(selector,SelectionKey.OP_READ);
listen();
}
public void listen()
{
while(true)
{
try
{
selector.select();//该方法阻塞,返回的是就绪的通道的个数
//如果程序接着执行,代表有通道就绪,即有事件发生,此时,选择器通过selectedKeys方法返回触发的选择键
//选择键,每触发一个事件,代表一个选择键被选择
Set<SelectionKey> keys=selector.selectedKeys();
for(SelectionKey key:keys)
{
keys.remove(key);
if(!key.isValid())
continue;
handelKey(key);
}
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
public void handelKey(SelectionKey key) throws IOException
{
if(key.isValid() && key.isReadable())
{
//获取触发key的通道
SocketChannel socketChannel=(SocketChannel)key.channel();
String content="";//空字符串的长度就是0
ByteBuffer buffer=ByteBuffer.allocate(1024);
buffer.clear();
int count=socketChannel.read(buffer);
if(count>0)
{
buffer.flip();
byte[] data=new byte[buffer.remaining()];
buffer.get(data);
content+=new String(data);
//处理读取到的数据
doReadData(socketChannel,content);
}
//设置下一次可读
key.interestOps(SelectionKey.OP_READ);
if(count==-1)
{
//count==-1代表客户端已经关闭,那这个键就没意义了,那么就取消改建,关闭客户端通道
key.cancel();//这句话的实际意思是,取消触发该键的通道到选择器的绑定注册
try
{
if(socketChannel!=null)
{
socketChannel.close();
}
}
catch(IOException e)
{
e.printStackTrace();
}
}
//处理读取到的数据,不该在这里处理
// doReadData(socketChannel,content);
}
}
public void doReadData(SocketChannel socketChannel,String content)
{
Message msg=JsonUtil.toBean(content);
String data=msg.getContent();
if(MessageConstants.REGISTER.equals(msg.getStyle()))
{
JOptionPane.showConfirmDialog(null,data);
}
if(MessageConstants.REGISTER_SUCCESS.equals(msg.getStyle()))
{
JOptionPane.showConfirmDialog(null,data);
//聊天对象窗口获取焦点
talk_nick.requestFocus();
}
if(MessageConstants.APPLY_SUCCESS.equals(msg.getStyle()))
{
JOptionPane.showConfirmDialog(null,data);
send_msg_area.setText("");
send_msg_area.requestFocus();
}
if(MessageConstants.APPLY_FIELD.equals(msg.getStyle()))
{
JOptionPane.showConfirmDialog(null,data);
talk_nick.setText("");
talk_nick.requestFocus();
}
if(MessageConstants.NORMAL.equals(msg.getStyle()))
{
String nick_from=msg.getSender_nick();
String date=msg.getSend_date();
//获取talk_nick
// String talk_nick_value=msg.getTalk_nick();
talk_nick.setText(nick_from);
// String content=msg.getContent();
String text=nick_from+" "+date+"\n"+data+"\n"+"\n";
show_msg_area.append(text);
}
}
public void initLisenter()
{
btn_register.addActionListener(this);
talk_btn.addActionListener(this);
close_btn.addActionListener(this);
clear_btn.addActionListener(this);
cancel_btn.addActionListener(this);
send_btn.addActionListener(this);
send_msg_area.addKeyListener(new KeyAdapter()
{
public void keyPressed(KeyEvent e)
{
if(e.getKeyCode()==KeyEvent.VK_ENTER)
{
send_msg();
}
}
//添加keyReleased方法,避免发送消息后系统认为ENTER键换行
public void keyReleased(KeyEvent e)
{
if(e.getKeyCode()==KeyEvent.VK_ENTER)
{
send_msg_area.setText("");
send_msg_area.requestFocusInWindow();
send_msg_area.setCaretPosition(0);
}
}
});
//退出按钮事件
exit_btn.addMouseListener(new MouseAdapter()
{
//鼠标进入事件
@Override
public void mouseEntered(MouseEvent e)
{
exit_btn.setIcon(PictureUtil.getImageIcon("close_active.png"));
}
//鼠标退出事件
public void mouseExited(MouseEvent e)
{
exit_btn.setIcon(PictureUtil.getImageIcon("close.png"));
}
//鼠标单击事件
public void mouseClicked(MouseEvent e)
{
//退出程序之前关闭通道对象
if(socketChannel!=null)
{
try
{
socketChannel.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
System.exit(0);
}
});
}
public void actionPerformed(ActionEvent e)
{
//判断事件源
if(e.getSource()==close_btn)
{ //退出程序之前关闭通道对象
if(socketChannel!=null)
{
try
{
socketChannel.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
System.exit(0);
}
if(e.getSource()==btn_register)
{
String nick=my_nick.getText();
if(nick.length()==0)
{
//注意,对话框也是阻塞的
JOptionPane.showMessageDialog(null,"注册昵称不能为空!","错误提示",JOptionPane.ERROR_MESSAGE);
//设置注册框获取焦点
my_nick.requestFocus();
}
else
{
String content=nick+"申请注册!";
Message msg=new Message(MessageConstants.REGISTER,content);
msg.setSender_nick(nick);
String msg_str=JsonUtil.toJson(msg);
//其实应该利用数据库判断这个nick是否重复
try {
socketChannel.write(ByteBuffer.wrap(msg_str.getBytes()));
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
if(e.getSource()==talk_btn)
{
String nick=talk_nick.getText();
if(nick.length()==0)
{
//注意,对话框也是阻塞的
JOptionPane.showMessageDialog(null,"聊天对方名称不能为空!","错误提示",JOptionPane.ERROR_MESSAGE);
//设置注册框获取焦点
talk_nick.requestFocus();
}
else
{
String content=my_nick.getText()+"申请与"+nick+"聊天!";
Message msg=new Message(MessageConstants.APPLY,content);
msg.setSender_nick(my_nick.getText());
msg.setTalk_nick(nick);
System.out.println(nick);
String msg_str=JsonUtil.toJson(msg);
//其实应该利用数据库判断这个nick是否重复
try {
socketChannel.write(ByteBuffer.wrap(msg_str.getBytes()));
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
if(e.getSource()==clear_btn)
{
show_msg_area.setText("");
}
if(e.getSource()==send_btn)
{
send_msg();
}
if(e.getSource()==cancel_btn)
{
send_msg_area.setText("");
send_msg_area.requestFocus();
}
}
public void send_msg()
{
String text=send_msg_area.getText();
if(text.length()==0)
{
//注意,对话框也是阻塞的
JOptionPane.showMessageDialog(null,"消息内容不能为空!","错误提示",JOptionPane.ERROR_MESSAGE);
//设置注册框获取焦点
send_msg_area.requestFocus();
}
else
{
String date=new SimpleDateFormat("yy年MM月dd日 HH时mm分ss秒").format(new Date());
String msg_text=my_nick.getText()+" "+date+"\n"+text+"\n"+"\n";
Message msg=new Message(MessageConstants.NORMAL,text);
msg.setSend_date(date);
msg.setSender_nick(my_nick.getText());
msg.setTalk_nick(talk_nick.getText());
String msg_str=JsonUtil.toJson(msg);
try {
socketChannel.write(ByteBuffer.wrap(msg_str.getBytes()));
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
// show_msg_area.setText(msg_text);
show_msg_area.append(msg_text);
send_msg_area.setText("");
send_msg_area.requestFocus();
}
}
public void initGui()
{
//设置大小
setSize(600,400);
//设置无边框
setUndecorated(true);
//设置可拖动
WindowDrag.allowDrag(this);
//设置居中
setLocationRelativeTo(null);
//设置UI风格,注意MAC中会报错,因为这个风格是windows专属的
try
{
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
}
catch(Exception e)
{
e.printStackTrace();
}
//设置默认关闭
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
//设置主面板用给定背景重绘
contentPane=new JPanel()
{
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
g.drawImage(PictureUtil.getImageIcon("back2.jpg").getImage(), 0,0,null);
this.setOpaque(false);
}
};
//添加到主面板的中间
getContentPane().add(contentPane,BorderLayout.CENTER);
//设置布局为空
contentPane.setLayout(null);
contentPane.setBorder(BorderFactory.createLineBorder(Color.GRAY));
top_pane=new JPanel();
BoxLayout box=new BoxLayout(top_pane,BoxLayout.X_AXIS);
top_pane.setLayout(box);
top_pane.add(new JLabel("我是",JLabel.CENTER));
top_pane.add(my_nick=new JTextField(10));
top_pane.add(btn_register=new JButton("注册"));
//添加间隔
top_pane.add(Box.createHorizontalStrut(5));
top_pane.add(new JLabel("对方姓名",JLabel.CENTER));
top_pane.add(Box.createHorizontalStrut(5));
top_pane.add(talk_nick=new JTextField(10));
top_pane.add(Box.createHorizontalStrut(5));
top_pane.add(talk_btn=new JButton("开始聊天"));
// top_pane.setPreferredSize(new Dimension(555,30));
//将顶部面板添加到主窗体的顶端(对话框默认为边界布局)
// getContentPane().add(top_pane,BorderLayout.NORTH);
contentPane.add(top_pane);
top_pane.setBounds(5,5,555,30);
//添加关闭标签
exit_btn=new JLabel();
contentPane.add(exit_btn);
exit_btn.setBounds(561,0,39,20);
exit_btn.setIcon(PictureUtil.getImageIcon("close.png"));
//显示消息面板添加到主窗体的中间
// getContentPane().add(show_msg_area=new JTextArea(),BorderLayout.CENTER);
//设置显示消息面板自动换行
show_msg_area=new JTextArea();
show_msg_area.setLineWrap(true);
show_msg_area.setWrapStyleWord(true);
//设置不可编辑
show_msg_area.setEditable(false);
//设置边框
// show_msg_area.setBorder(BorderFactory.createLineBorder(Color.RED));
TitledBorder tb=BorderFactory.createTitledBorder("显示消息面板");
tb.setTitleColor(Color.red);
tb.setTitleJustification(TitledBorder.CENTER);
show_msg_area.setBorder(tb);
// show_msg_area.setPreferredSize(new Dimension(550,230));
//滚动条?将JTextArea放入JScrollPane即可
contentPane.add(show_msg_area);
show_msg_area.setBounds(25,45,550,230);
//初始化底部面板,设置为边界布局
bottom_pane=new JPanel(new BorderLayout());
//承载底部面板按钮的子面板,流式布局
JPanel btn_pane=new JPanel();
btn_pane.add(close_btn=new JButton("关闭"));
btn_pane.add(clear_btn=new JButton("清空消息"));
btn_pane.add(cancel_btn=new JButton("取消发送"));
btn_pane.add(send_btn=new JButton("发送"));
bottom_pane.add(send_msg_area=new JTextArea(),BorderLayout.CENTER);
//设置发送消息面板自动换行,并显示滚动条
send_msg_area.setLineWrap(true);
//设置换行不断字
send_msg_area.setWrapStyleWord(true);
//设置边界
// send_msg_area.setBorder(BorderFactory.createLineBorder(Color.BLUE));
TitledBorder tb_2=BorderFactory.createTitledBorder("发送消息面板");
tb_2.setTitleJustification(TitledBorder.CENTER);
tb_2.setTitleColor(Color.red);
send_msg_area.setBorder(tb_2);
bottom_pane.add(btn_pane,BorderLayout.SOUTH);
// bottom_pane.setPreferredSize(new Dimension(550,120));
contentPane.add(bottom_pane);
bottom_pane.setBounds(25,285,550,110);
//将底部面板添加到主窗体的最下面
// getContentPane().add(bottom_pane,BorderLayout.SOUTH);
//设置可见
setVisible(true);
}
public static void main(String[] args) throws IOException
{
new ClientByNio();
}
}
客户端可以运行多了,单个客户端关闭不影响其他客户端运行,但是注意,服务端通道我没做单独处理,如果在没有关闭客户端的情况下强行关闭服务端,会出现无限制的IOException,以为客户端通道一直在读取。
实现图片如下,客户端启动界面(启动客户端前必须启动服务端,服务端没有用界面):
客户端注册成功界面:
可启动多个客户端
申请聊天成功界面(如果输入对方的nick不存在,则会出现申请聊天失败提示对话框)
开始聊天