您的位置:首页 > Web前端

Java NIO SocketChannel+Buffer+Selector 详解(含多人聊天室实例)

2018-03-13 20:21 696 查看

一、Java NIO 的核心组件

Java NIO的核心组件包括:Channel(通道),Buffer(缓冲区),Selector(选择器),其中Channel和Buffer比较好理解 
简单来说 NIO是面向通道和缓冲区的,意思就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中。关于Channel 和 Buffer的详细讲解请看:Java NIO 教程

二、Java NIO Selector

1. Selector简介

选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。在开始之前,需要回顾一下Selector、SelectableChannel和SelectionKey:

选择器(Selector)

Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

可选择通道(SelectableChannel)

SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。因为FileChannel类没有继承SelectableChannel因此是不是可选通道,而所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

选择键(SelectionKey)

选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。下面是使用Selector管理多个channel的结构图: 


2. Selector的使用

(1)创建Selector

Selector对象是通过调用静态工厂方法open()来实例化的,如下:
Selector Selector=Selector.open();
类方法open()实际上向SPI1发出请求,通过默认的SelectorProvider对象获取一个新的实例。

(2)将Channel注册到Selector

一个Channel可以注册到多个Selector中,但是每个Selector只能注册一回。要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:
channel.configureBlocking(false);
SelectionKey key= channel.register(selector,SelectionKey.OP_READ);
//selector在轮询时,只关注这个channel的SelectionKey是否可读
调用通道的register()方法会将它注册到一个选择器上。与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常,这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字通道都可以。另外通道一旦被注册,将不能再回到阻塞状态,此时若调用通道的configureBlocking(true)将抛出BlockingModeException异常。
register()方法的第二个参数是“interest集合”,表示选择器所关心的通道操作,它实际上是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。比如一个选择器对通道的read和write操作感兴趣,那么选择器在检查该通道时,只会检查通道的read和write操作是否已经处在就绪状态。 
它有以下四种操作类型:Connect 连接
Accept 接受
Read 读
Write 写
需要注意并非所有的操作在所有的可选择通道上都能被支持,比如ServerSocketChannel支持Accept,而SocketChannel中不支持。我们可以通过通道上的validOps()方法来获取特定通道下所有支持的操作集合。Java中定义了四个常量来表示这四种操作类型:SelectionKey.OP_CONNECT 
SelectionKey.OP_ACCEPT 
SelectionKey.OP_READ 
SelectionKey.OP_WRITE如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:
int interestSet=SelectionKey.OP_READ|SelectionKey.OP_WRITE; 
当通道触发了某个操作之后,表示该通道的某个操作已经就绪,可以被操作。因此,某个SocketChannel成功连接到另一个服务器称为“连接就绪”(OP_CONNECT)。一个ServerSocketChannel准备好接收新进入的连接称为“接收就绪”(OP_ACCEPT)。一个有数据可读的通道可以说是“读就绪”(OP_READ)。等待写数据的通道可以说是“写就绪”(OP_WRITE)。我们注意到register()方法会返回一个SelectionKey对象,我们称之为键对象。该对象包含了以下四种属性:interest集合  //Selector关注此Channel(SelectionKey)的事件总集合
read集合      //此Channel(SelectionKey)已经就绪的事件集合
Channel       //此SelectionKey对应的Channel
Selector       //此SelectionKey对应的Selector
interest集合是Selector感兴趣的集合,用于指示选择器对通道关心的操作。即此Channel注册进入Selector时设置的关注事件(Connect/Accept/Read/Write)。可通过SelectionKey对象的interestOps()获取。最初,该兴趣集合是通道被注册到Selector时传进来的值。该集合不会被选择器改变,但是可通过interestOps()改变。我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:
int interestSet=selectionKey.interestOps();
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
read集合是通道已经就绪的操作的集合,表示一个通道准备好要执行的操作了,可通过SelctionKey对象的readyOps()来获取相关通道已经就绪的操作。它是interest集合的子集,并且表示了interest集合中从上次调用select()以后已经就绪的那些操作。(比如选择器对通道的read,write操作感兴趣(interest集合),而某时刻通道的write操作已经准备就绪可以被选择器获知了(read集合),前一种就是interest集合,后一种则是read集合。)。JAVA中定义以下几个方法用来检查这些操作是否就绪:
//int readSet=selectionKey.readOps();
selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
需要注意的是,通过相关的选择键的readyOps()方法返回的就绪状态指示只是一个提示,底层的通道在任何时候都会不断改变,而其他线程也可能在通道上执行操作并影响到它的就绪状态。另外,我们不能直接修改read集合。
取出SelectionKey所关联的Selector和Channel 
通过SelectionKey访问对应的Selector和Channel:
Channel channel =selectionKey.channel();
Selector selector=selectionKey.selector();
关于取消SelectionKey对象的那点事我们可以通过SelectionKey对象的cancel()方法来取消特定的注册关系。该方法调用之后,该SelectionKey对象将会被”拷贝”至已取消键的集合中,该键此时已经失效,但是该注册关系并不会立刻终结。在下一次select()时,已取消键的集合中的元素会被清除,相应的注册关系也真正终结。

(3)为SelectionKey绑定附加对象

SelectionKey绑定Object很重要,Object的数据结构设计要注意以聊天程序服务器的ServerSocketChannel,处理一个SocketChannel向另一个SocketChannel发消息为例:
1 管理用户名id和其对应SelectionKey(SocketChannel)
  a.可以建立一个MAP容器作为用户名单,当SocketChannel注册进来时将其用户名和其SelectionKey作为K,V保存进MAP。
  b.或者将其用户名放到SelectionKey的绑定对象的属性中。
  绑定对象格式: class Bind{  String userid  ; List<Write> writelist;//待写内容}  class Write{ String fromid;String txt }

2 Selector 中注册有一个ServerSocketChannel和N个SocketChannel,Selector轮询发现A可读,通过A的SelectionKey.channel()获得A的SocketChannel
并执行read(buffer)读到buffer中解析内容(内容格式由开发者自定义,例如JSON,可以包含内容和收信人等),获得收信人信息,
遍历SelectionKey.Selector().selectedKeys()找到收件人的SelectionKey,将内容绑定给此SelectionKey。当收信人SelectionKey进入可写状态时
取出绑定内容,写给对应的收信人SocketChannel
1 在注册的时候直接绑定:
SelectionKey key=channel.register(selector,SelectionKey.OP_READ,theObject);2 在绑定完成之后附加:selectionKey.attach(theObject);//绑定绑定之后,可通过对应的SelectionKey取出该对象:selectionKey.attachment();。如果要取消该对象,则可以通过该种方式:selectionKey.attach(null).需要注意的是如果附加的对象不再使用,一定要人为清除,因为垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。一个单独的通道可被注册到多个选择器中,有些时候我们需要通过isRegistered()方法来检查一个通道是否已经被注册到任何一个选择器上。 通常来说,我们并不会这么做。

(4)通过Selector选择通道

我们知道选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中。接下来我们简单的了解一下Selector维护的三种类型SelectionKey集合:已注册的键的集合(Registered key set)所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys()方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。已选择的键的集合(Selected key set)已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(并有可能是空的)。 
不要将已选择的键的集合与ready集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。已取消的键的集合(Cancelled key set)已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。在刚初始化的Selector对象中,这三个集合都是空的。通过Selector的select()方法可以选择已经准备就绪的通道(这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:select():阻塞到至少有一个通道在你注册的事件上就绪了。 
select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒。 
selectNow():非阻塞,只要有通道就绪就立刻返回。select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合。如下: 
Set selectedKeys=selector.selectedKeys(); 
进而可以放到和某SelectionKey关联的Selector和Channel。如下所示:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
关于Selector执行选择的过程
我们知道调用select()方法进行通道,现在我们再来深入一下选择的过程,也就是select()执行过程。当select()被调用时将执行以下几步:首先检查已取消键集合,也就是通过cancle()取消的键。如果该集合不为空,则清空该集合里的键,同时该集合中每个取消的键也将从已注册键集合和已选择键集合中移除。(一个键被取消时,并不会立刻从集合中移除,而是将该键“拷贝”至已取消键集合中,这种取消策略就是我们常提到的“延迟取消”。)
再次检查已注册键集合(准确说是该集合中每个键的interest集合)。系统底层会依次询问每个已经注册的通道是否准备好选择器所感兴趣的某种操作,一旦发现某个通道已经就绪了,则会首先判断该通道是否已经存在在已选择键集合当中,如果已经存在,则更新该通道在已注册键集合中对应的键的ready集合,如果不存在,则首先清空该通道的对应的键的ready集合,然后重设ready集合,最后将该键存至已注册键集合中。这里需要明白,当更新ready集合时,在上次select()中已经就绪的操作不会被删除,也就是ready集合中的元素是累积的,比如在第一次的selector对某个通道的read和write操作感兴趣,在第一次执行select()时,该通道的read操作就绪,此时该通道对应的键中的ready集合存有read元素,在第二次执行select()时,该通道的write操作也就绪了,此时该通道对应的ready集合中将同时有read和write元素。
深入已注册键集合的管理到现在我们已经知道一个通道的的键是如何被添加到已选择键集合中的,下面我们来继续了解对已选择键集合的管理 。首先要记住:选择器不会主动删除被添加到已选择键集合中的键,而且被添加到已选择键集合中的键的ready集合只能被设置,而不能被清理。如果我们希望清空已选择键集合中某个键的ready集合该怎么办?我们知道一个键在新加入已选择键集合之前会首先置空该键的ready集合,这样的话我们可以人为的将某个键从已注册键集合中移除最终实现置空某个键的ready集合。被移除的键如果在下一次的select()中再次就绪,它将会重新被添加到已选择的键的集合中。这就是为什么要在每次迭代的末尾调用keyIterator.remove()。

(5)停止选择

选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回 
该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
通过close()方法关闭Selector** 
该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。
调用interrupt() 
调用该方法会使睡眠的线程抛出InterruptException异常,捕获该异常并在调用wakeup()
上面有些人看到“系统底层会依次询问每个通道”时可能在想如果已选择键非常多是,会不会耗时较长?答案是肯定的。但是我想说的是通常你可以选择忽略该过程,至于为什么,后面再说。

三、NIO多人聊天室

服务端
public class ChatServer implements Runnable{

private Selector selector;
private SelectionKey serverKey;
private Vector<String> usernames;
private static final int PORT = 9999;

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public ChatServer(){
usernames = new Vector<String>();
init();
}

public void init(){
try {
selector = Selector.open();
//创建serverSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket socket = serverChannel.socket();
socket.bind(new InetSocketAddress(PORT));
//加入到selector中
serverChannel.configureBlocking(false);
serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
printInfo("server starting.......");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void run() {
try {
while(true){
//获取就绪channel
int count = selector.select();
if(count > 0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();

//若此key的通道是等待接受新的套接字连接
if(key.isAcceptable()){
System.out.println(key.toString() + " : 接收");
//一定要把这个accpet状态的服务器key去掉,否则会出错
iterator.remove();
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
//接受socket
SocketChannel socket = serverChannel.accept();
socket.configureBlocking(false);
//将channel加入到selector中,并一开始读取数据
socket.register(selector, SelectionKey.OP_READ);
}
//若此key的通道是有数据可读状态
if(key.isValid() && key.isReadable()){
System.out.println(key.toString() + " : 读");
readMsg(key);
}
//若此key的通道是写数据状态
if(key.isValid() && key.isWritable()){
System.out.println(key.toString() + " : 写");
writeMsg(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

private void readMsg(SelectionKey key) {
SocketChannel channel = null;
try {
channel = (SocketChannel) key.channel();
//设置buffer缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//假如客户端关闭了通道,这里在对该通道read数据,会发生IOException,捕获到Exception后,关闭掉该channel,取消掉该key
int count = channel.read(buffer);
StringBuffer buf = new StringBuffer();
//如果读取到了数据
if(count > 0){
//让buffer翻转,把buffer中的数据读取出来
buffer.flip();
buf.append(new String(buffer.array(), 0, count));
}
String msg = buf.toString();

//如果此数据是客户端连接时发送的数据
if(msg.indexOf("open_") != -1){
String name = msg.substring(5);//取出名字
printInfo(name + " --> online");
usernames.add(name);
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey skey = iter.next();
//若不是服务器套接字通道的key,则将数据设置到此key中
//并更新此key感兴趣的动作
if(skey != serverKey){
skey.attach(usernames);
skey.interestOps(skey.interestOps() | SelectionKey.OP_WRITE);
}
}
//如果是下线时发送的数据
}else if(msg.indexOf("exit_") != -1){
String username = msg.substring(5);
usernames.remove(username);
key.attach("close");
//要退出的当前channel加上close的标示,并把兴趣转为写,如果write中收到了close,则中断channel的链接
key.interestOps(SelectionKey.OP_WRITE);
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey sKey = iter.next();
sKey.attach(usernames);
sKey.interestOps(sKey.interestOps() | SelectionKey.OP_WRITE);
}
//如果是聊天发送数据
}else{
String uname = msg.substring(0, msg.indexOf("^"));
msg = msg.substring(msg.indexOf("^") + 1);
printInfo("("+uname+")说:" + msg);
String dateTime = sdf.format(new Date());
String smsg = uname + " " + dateTime + "\n  " + msg + "\n";
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey sKey = iter.next();
sKey.attach(smsg);
sKey.interestOps(sKey.interestOps() | SelectionKey.OP_WRITE);
}
}
buffer.clear();
} catch (IOException e) {
//当客户端关闭channel时,服务端再往通道缓冲区中写或读数据,都会报IOException,解决方法是:在服务端这里捕获掉这个异常,并且关闭掉服务端这边的Channel通道
key.cancel();
try {
channel.socket().close();
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}

private void writeMsg(SelectionKey key) {
try {
SocketChannel channel = (SocketChannel) key.channel();
Object attachment = key.attachment();
//获取key的值之后,要把key的值置空,避免影响下一次的使用
key.attach("");
channel.write(ByteBuffer.wrap(attachment.toString().getBytes()));
key.interestOps(SelectionKey.OP_READ);

} catch (Exception e) {
e.printStackTrace();
}
}

private void printInfo(String str) {
System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
}

public static void main(String[] args) {
ChatServer server = new ChatServer();
new Thread(server).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
注意这里readMsg 和 writeMsg中,read操作的key重新设置interest要遍历所有key,而write操作的key重新设置interest只需要设置传入的当前key,原因: 
读操作之所以要遍历key,是因为这里channel的读写操作的流程是: 
1. read到数据后,把数据加到每一个key的attach中 
2. 写数据时,从key的attach中取出数据,从而把该数据写到buffer中例如:当选择器有3个channel的情况下,实现多人聊天,流程: 
1. 其中一个channel发送数据,该channel接受到数据 
2. 在该channel的读操作中,遍历所有的channel,为每一个channel的attach加上该数据 
3. 每一个channel在写操作时,从key的attach中取出数据,分别把该数据写到各自的buffer中 
4. 于是每一个channel的界面都能看到其中一个channel发送的数据客户端:
public class ChatClient {

private static final String HOST = "127.0.0.1";
private static int PORT = 9999;
private static SocketChannel socket;
private static ChatClient client;

private static byte[] lock = new byte[1];
//单例模式管理
private ChatClient() throws IOException{
socket = SocketChannel.open();
socket.connect(new InetSocketAddress(HOST, PORT));
socket.configureBlocking(false);
}

public static ChatClient getIntance(){
synchronized(lock){
if(client == null){
try {
client = new ChatClient();
} catch (IOException e) {
e.printStackTrace();
}
}
return client;
}
}

public void sendMsg(String msg){
try {
socket.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}

public String receiveMsg(){
String msg = null;
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuffer buf = new StringBuffer();
int count = 0;
//不一定一次就能读满,连续读
while((count = socket.read(buffer)) > 0){
buf.append(new String(buffer.array(), 0, count));
}
//有数据
if(buf.length() > 0){
msg = buf.toString();
if(buf.toString().equals("close")){
//不过不sleep会导致ioException的发生,因为如果这里直接关闭掉通道,在server里,
//该channel在read(buffer)时会发生读取异常,通过sleep一段时间,使得服务端那边的channel先关闭,客户端
//的channel后关闭,这样就能防止read(buffer)的ioException
//但是这是一种笨方法
//Thread.sleep(100);
//更好的方法是,在readBuffer中捕获异常后,手动进行关闭通道
socket.socket().close();
socket.close();
msg = null;
}
}
} catch (IOException e) {
e.printStackTrace();
}
return msg;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
界面代码:设置姓名
public class SetNameFrame extends JFrame {
private static final long serialVersionUID = 1L;
private static JTextField txtName;
private static JButton btnOK;
private static JLabel label;

public SetNameFrame() {
this.setLayout(null);
Toolkit kit = Toolkit.getDefaultToolkit();
int w = kit.getScreenSize().width;
int h = kit.getScreenSize().height;
this.setBounds(w / 2 - 230 / 2, h / 2 - 200 / 2, 230, 200);
this.setTitle("设置名称");
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.setResizable(false);
txtName = new JTextField(4);
this.add(txtName);
txtName.setBounds(10, 10, 100, 25);
btnOK = new JButton("OK");
this.add(btnOK);
btnOK.setBounds(120, 10, 80, 25);
label = new JLabel("[w:" + w + ",h:" + h + "]");
this.add(label);
label.setBounds(10, 40, 200, 100);
label.setText("<html>在上面的文本框中输入名字<br/>显示器宽度:" + w + "<br/>显示器高度:" + h
+ "</html>");

btnOK.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String uname = txtName.getText();
ChatClient service = ChatClient.getIntance();
ChatFrame chatFrame = new ChatFrame(service, uname);
chatFrame.show();
setVisible(false);
}
});
}

public static void main(String[] args) {
SetNameFrame setNameFrame = new SetNameFrame();
setNameFrame.setVisible(true);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
界面代码:聊天界面
public class ChatFrame {

private JTextArea readContext = new JTextArea(18, 30);// 显示消息文本框
private JTextArea writeContext = new JTextArea(6, 30);// 发送消息文本框

private DefaultListModel modle = new DefaultListModel();// 用户列表模型
private JList list = new JList(modle);// 用户列表

private JButton btnSend = new JButton("发送");// 发送消息按钮
private JButton btnClose = new JButton("关闭");// 关闭聊天窗口按钮

private JFrame frame = new JFrame("ChatFrame");// 窗体界面

private String uname;// 用户姓名

private ChatClient service;// 用于与服务器交互

private boolean isRun = false;// 是否运行

public ChatFrame(ChatClient service, String uname) {
this.isRun = true;
this.uname = uname;
this.service = service;
}

// 初始化界面控件及事件
private void init() {
frame.setLayout(null);
frame.setTitle(uname + " 聊天窗口");
frame.setSize(500, 500);
frame.setLocation(400, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
JScrollPane readScroll = new JScrollPane(readContext);
readScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
frame.add(readScroll);
JScrollPane writeScroll = new JScrollPane(writeContext);
writeScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
frame.add(writeScroll);
frame.add(list);
frame.add(btnSend);
frame.add(btnClose);
readScroll.setBounds(10, 10, 320, 300);
readContext.setBounds(0, 0, 320, 300);
readContext.setEditable(false);
readContext.setLineWrap(true);// 自动换行
writeScroll.setBounds(10, 315, 320, 100);
writeContext.setBounds(0, 0, 320, 100);
writeContext.setLineWrap(true);// 自动换行
list.setBounds(340, 10, 140, 445);
btnSend.setBounds(150, 420, 80, 30);
btnClose.setBounds(250, 420, 80, 30);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
isRun = false;
service.sendMsg("exit_" + uname);
System.exit(0);
}
});

btnSend.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String msg = writeContext.getText().trim();
if(msg.length() > 0){
service.sendMsg(uname + "^" + writeContext.getText());
}
writeContext.setText(null);
writeContext.requestFocus();
}
});

btnClose.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
isRun = false;
service.sendMsg("exit_" + uname);
System.exit(0);
}
});

list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
// JOptionPane.showMessageDialog(null,
// list.getSelectedValue().toString());
}
});

writeContext.addKeyListener(new KeyListener() {

@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub

}

@Override
public void keyReleased(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_ENTER){
String msg = writeContext.getText().trim();
if(msg.length() > 0){
service.sendMsg(uname + "^" + writeContext.getText());
}
writeContext.setText(null);
writeContext.requestFocus();
}
}

@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub

}
});
}

// 此线程类用于轮询读取服务器发送的消息
private class MsgThread extends Thread {
@Override
public void run() {
while (isRun) {
String msg = service.receiveMsg();
if (msg != null) {
//如果存在[],这是verctor装的usernames的toString生成的
if (msg.indexOf("[") != -1 && msg.lastIndexOf("]") != -1) {
msg = msg.substring(1, msg.length() - 1);
String[] userNames = msg.split(",");
modle.removeAllElements();
for (int i = 0; i < userNames.length; i++) {
modle.addElement(userNames[i].trim());
}
} else {//如果是普通的消息
String str = readContext.getText() + msg;
readContext.setText(str);
readContext.selectAll();
}
}
}
}
}

// 显示界面
public void show() {
this.init();
service.sendMsg("open_" + uname);
MsgThread msgThread = new MsgThread();
msgThread.start();
this.frame.setVisible(true);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

分析整个程序的流程:

只有一个客户端连接的注释:[2017-01-23 21:26:14] -> server starting……. 
sun.nio.ch.SelectionKeyImpl@99436c6 : 接收 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 读 
[2017-01-23 21:26:19] -> a –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写可以看出流程是:服务端接受通道 -> 通道进行读操作 -> 通道进行写操作 
1. 当客户端的channel调用connect后,服务端接受到该Channel,于是把该通道的兴趣改为read就绪 
2. 客户端connect后,立马写数据”open_”到通道缓冲区中,于是该通道进入了有数据可读状态(即读状态),且该通道的兴趣为read,所以select()的返回值为1,进入了readMsg(); 
3. readMsg中把每一个key的状态改为了写状态,而此时客户端一直在read数据,要求你服务端要给我数据,于是服务器的channel此时是写状态,且该通道的兴趣为write,所以select()的返回值为1,进入了writeMsg();有两个个客户端连接的注释:sun.nio.ch.SelectionKeyImpl@99436c6 : 接收 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 读 
[2017-01-23 21:26:19] -> a –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写 
sun.nio.ch.SelectionKeyImpl@99436c6 : 接收 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 读 
[2017-01-23 21:32:30] -> b –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 写 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写可以看到,@99436c6是ServerSocketChannel,@3ee5015是第一个链接的Channel,@12cb94b7是第二个连接的Channel,可以看见,第二个Channel连接之后sun.nio.ch.SelectionKeyImpl@3ee5015 : 写 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 读 
[2017-01-23 21:32:30] -> b –> online 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写 
sun.nio.ch.SelectionKeyImpl@12cb94b7 : 写 
sun.nio.ch.SelectionKeyImpl@3ee5015 : 写两个Channel是交替运行的,说明Selector处理Channle,是轮询处理的
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: