说是简单聊天系统,压根不能算是一个系统,顶多算个雏形。本文重点不在聊天系统设计和实现上,而是通过实现类似效果,展示下NIO 和Socket两种编程方式的差异性。说是Socket与NIO的编程方式,不太严谨,因为NIO的底层也是通过Socket实现的,但又想不出非常好的题目,就这样吧。
主要内容
Socket方式实现简易聊天效果
NIO方式实现简易聊天效果
两种方式的性能对比
前言
预期效果,是客户端之间进行“广播”式聊天,类似于QQ群聊天。希望以后有机会,以此简易版为基础,不断演进,演练下在线聊天系统。
1.Socket方式实现简易聊天效果
1.1服务端 Server.java
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
|
package
com.example.socket.server;
import
java.io.IOException;
import
java.net.InetAddress;
import
java.net.ServerSocket;
import
java.net.Socket;
import
java.util.ArrayList;
import
java.util.List;
public
class
Server {
private
static
int
port =
9999
;
// 可接受请求队列的最大长度
private
static
int
backlog=
100
;
// 绑定到本机的IP地址
private
static
final
String bindAddr =
"127.0.0.1"
;
//socket字典列表
private
static
List<Socket> nodes=
new
ArrayList<Socket>();
public
static
void
main(String[] args) {
try
{
ServerSocket ss =
new
ServerSocket(port, backlog,InetAddress.getByName(bindAddr));
for
(;;){
//发生阻塞,等待客户端连接
Socket sc = ss.accept();
nodes.add(sc);
InetAddress addr = sc.getLocalAddress();
System.out.println(
"create new session from "
+addr.getHostName()+
":"
+sc.getPort()+
"\n"
);
//针对一个Socket 客户端 启动两个线程,分别是接收信息,发送信息
new
Thread(
new
ServerMessageReceiver(sc,nodes)).start();
new
ServerMessageSender(sc).start();
}
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
|
1.2 消息接收端 ServerMessageReceiver.java
额外负责信息广播
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
|
package
com.example.socket.server;
import
java.io.BufferedReader;
import
java.io.BufferedWriter;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.io.OutputStreamWriter;
import
java.net.Socket;
import
java.util.ArrayList;
import
java.util.List;
/**
*
* 接收消息
*
*/
public
class
ServerMessageReceiver
implements
Runnable{
private
Socket socket;
//socket字典列表
private
List<Socket> nodes=
new
ArrayList<Socket>();
public
ServerMessageReceiver(Socket sc,List<Socket> nodes){
this
.socket=sc;
this
.nodes=nodes;
}
/**
* 信息广播到其他节点
*/
@Override
public
void
run() {
try
{
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(socket.getInputStream(),
"UTF-8"
));
//接收到的消息
String content;
while
(
true
) {
if
(socket.isClosed()){
System.out.println(
"Socket已关闭,无法获取消息"
);
reader.close();
socket.close();
break
;
}
content=reader.readLine();
if
(content!=
null
&& content.equals(
"bye"
)){
System.out.println(
"对方请求关闭连接,无法继续进行聊天"
);
reader.close();
socket.close();
break
;
}
String message =socket.getPort()+
":"
+content;
//广播信息
for
(Socket n:
this
.nodes){
if
(n !=
this
.socket){
BufferedWriter writer=
new
BufferedWriter(
new
OutputStreamWriter(n.getOutputStream(),
"UTF-8"
));
writer.write(message);
writer.newLine();
writer.flush();
}
}
}
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
|
1.3消息发送服务端 ServerMessageSender.java
主要作用:发送欢迎信息
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
|
package
com.example.socket.server;
import
java.io.BufferedWriter;
import
java.io.IOException;
import
java.io.OutputStreamWriter;
import
java.net.Socket;
public
class
ServerMessageSender
extends
Thread{
private
Socket socket;
public
ServerMessageSender(Socket socket) {
this
.socket = socket;
}
/**
* 只发送一个欢迎信息
*/
@Override
public
void
run() {
try
{
BufferedWriter writer=
new
BufferedWriter(
new
OutputStreamWriter(socket.getOutputStream(),
"UTF-8"
));
// BufferedReader inputReader=new BufferedReader(new InputStreamReader(System.in));
try
{
String msg=
"server :welcome "
+socket.getPort();
writer.write(msg);
writer.newLine();
writer.flush();
}
catch
(IOException e) {
e.printStackTrace();
}
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
1.4 客户端 Client.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package
com.example.socket.client;
import
java.net.InetAddress;
import
java.net.Socket;
public
class
Client {
// 监听端口号
private
static
final
int
port =
9999
;
// 绑定到本机的IP地址
private
static
final
String bindAddr =
"127.0.0.1"
;
public
static
void
main(String[] args) {
try
{
System.out.println(
"正在连接Socket服务器"
);
Socket socket=
new
Socket(InetAddress.getByName(bindAddr),port);
System.out.println(
"已连接\n=================================="
);
new
ClientMessageSender(socket).start();
new
ClientMessageReceiver(socket).start();
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
1.4 消息接收客户端 ClientMessageReceiver.java
仅仅是输出
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
|
package
com.example.socket.client;
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.net.Socket;
public
class
ClientMessageReceiver
extends
Thread {
private
Socket socket;
public
ClientMessageReceiver(Socket socket) {
this
.socket=socket;
}
@Override
public
void
run() {
try
{
// 获取socket的输 出\入流
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(socket.getInputStream(),
"UTF-8"
));
//接收到的消息
String content;
while
(
true
) {
if
(socket.isClosed()){
System.out.println(
"Socket已关闭,无法获取消息"
);
reader.close();
socket.close();
break
;
}
content=reader.readLine();
if
(content.equals(
"bye"
)){
System.out.println(
"对方请求关闭连接,无法继续进行聊天"
);
reader.close();
socket.close();
break
;
}
System.out.println(content+
"\n"
);
}
reader.close();
socket.close();
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
1.5 消息发送客户端 ClientMessageSender.java
通过输入流输入,将信息传入Socket
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
|
package
com.example.socket.client;
import
java.io.BufferedReader;
import
java.io.BufferedWriter;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.io.OutputStreamWriter;
import
java.net.Socket;
public
class
ClientMessageSender
extends
Thread {
private
Socket socket;
public
ClientMessageSender(Socket socket) {
this
.socket = socket;
}
@Override
public
void
run() {
try
{
BufferedWriter writer=
new
BufferedWriter(
new
OutputStreamWriter(socket.getOutputStream(),
"UTF-8"
));
BufferedReader inputReader=
new
BufferedReader(
new
InputStreamReader(System.in));
try
{
String msg;
for
(;;){
msg=inputReader.readLine();
if
(msg.toLowerCase().equals(
"exit"
)){
System.exit(
0
);
}
if
(socket.isClosed()){
System.out.println(
"Socket已关闭,无法发送消息"
);
writer.close();
socket.close();
break
;
}
writer.write(msg);
writer.newLine();
writer.flush();
System.out.println();
}
}
catch
(IOException e) {
e.printStackTrace();
}
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
1.6 效果
2.NIO方式实现简易聊天效果
2.1服务端 NServer.java
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
|
package
com.example.nio;
import
java.io.IOException;
import
java.net.InetSocketAddress;
import
java.nio.ByteBuffer;
import
java.nio.channels.Channel;
import
java.nio.channels.SelectionKey;
import
java.nio.channels.Selector;
import
java.nio.channels.ServerSocketChannel;
import
java.nio.channels.SocketChannel;
import
java.nio.charset.Charset;
/**
* 服务器端
*/
public
class
NServer {
private
Selector selector;
private
Charset charset = Charset.forName(
"UTF-8"
);
public
void
init()
throws
Exception {
selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa =
new
InetSocketAddress(
"127.0.0.1"
,
3000
);
server.socket().bind(isa);
server.configureBlocking(
false
);
server.register(selector, SelectionKey.OP_ACCEPT);
while
(selector.select() >
0
) {
for
(SelectionKey key : selector.selectedKeys()) {
selector.selectedKeys().remove(key);
if
(key.isAcceptable()) {
SocketChannel sc = server.accept();
System.out.println(
"create new session from "
+sc.getRemoteAddress()+
"\n"
);
sc.configureBlocking(
false
);
sc.register(selector, SelectionKey.OP_READ);
key.interestOps(SelectionKey.OP_ACCEPT);
sc.write(charset.encode(
"welcome"
+sc.getRemoteAddress()));
}
if
(key.isReadable()) {
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buff = ByteBuffer.allocate(
1024
);
String content =
""
;
try
{
while
(sc.read(buff) >
0
) {
buff.flip();
content += charset.decode(buff);
buff.clear();
}
key.interestOps(SelectionKey.OP_READ);
}
catch
(IOException e) {
key.cancel();
if
(key.channel() !=
null
)
key.channel().close();
}
if
(content.length() >
0
) {
for
(SelectionKey sk : selector.keys()) {
Channel targetchannel = sk.channel();
if
(targetchannel
instanceof
SocketChannel && targetchannel!=sc) {
SocketChannel dest = (SocketChannel)targetchannel;
dest.write(charset.encode(content));
}
}
}
}
}
}
}
public
static
void
main(String[] args)
throws
Exception {
new
NServer().init();
}
}
|
2.2 客户端 NClient.java
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
|
package
com.example.nio;
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.nio.charset.Charset;
import
java.util.Scanner;
/**
* 客户端
*/
public
class
NClient {
private
Selector selector;
private
Charset charset = Charset.forName(
"UTF-8"
);
private
SocketChannel sc =
null
;
public
void
init()
throws
IOException {
selector = Selector.open();
InetSocketAddress isa =
new
InetSocketAddress(
"127.0.0.1"
,
3000
);
sc = SocketChannel.open(isa);
sc.configureBlocking(
false
);
sc.register(selector, SelectionKey.OP_READ);
new
ClientThread().start();
@SuppressWarnings
(
"resource"
)
Scanner scan =
new
Scanner(System.in);
while
(scan.hasNextLine()) {
sc.write(charset.encode(scan.nextLine()));
}
}
private
class
ClientThread
extends
Thread {
public
void
run() {
try
{
while
(selector.select() >
0
) {
for
(SelectionKey sk : selector.selectedKeys()) {
selector.selectedKeys().remove(sk);
if
(sk.isReadable()) {
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buff = ByteBuffer.allocate(
1024
);
String content =
""
;
while
(sc.read(buff) >
0
) {
sc.read(buff);
buff.flip();
content += charset.decode(buff);
buff.clear();
}
System.out.println(
"chat info: "
+ content);
sk.interestOps(SelectionKey.OP_READ);
}
}
}
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
public
static
void
main(String[] args)
throws
IOException {
new
NClient().init();
}
}
|
代码来自
https://github.com/xeostream/chat
2.3 效果
3. 对比
从API操作上来看,NIO偏复杂,面向的是异步编程方式,重点围绕Selector,SelectKey操作。
性能对比,主要简单模拟下Echo情景:客户端连接成功,服务端返回一条信息。
3.1Socket性能测试入口
可以关闭ServerMessageReceiver线程
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
|
package
com.example.socket.client;
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.net.InetAddress;
import
java.net.Socket;
import
java.net.UnknownHostException;
import
java.util.concurrent.Callable;
import
java.util.concurrent.ExecutionException;
import
java.util.concurrent.Executors;
import
java.util.concurrent.Future;
public
class
BenchmarkClient {
// 监听端口号
private
static
final
int
port =
9999
;
// 绑定到本机的IP地址
private
static
final
String bindAddr =
"127.0.0.1"
;
/**
* @param <T>
* @param args
*/
public
static
<T>
void
main(String[] args) {
try
{
long
s=System.currentTimeMillis();
for
(
int
i =
0
; i <
1000
; i++) {
final
Socket socket =
new
Socket(
InetAddress.getByName(bindAddr), port);
Future<String> future = Executors.newFixedThreadPool(
4
).submit(
new
Callable<String>() {
@Override
public
String call()
throws
Exception {
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(socket
.getInputStream(),
"UTF-8"
));
String content = reader.readLine();
return
Thread.currentThread().getName()+
"--->"
+content;
}
});
System.out.println(i+
":"
+future.get());
socket.close();
}
long
e=System.currentTimeMillis();
System.out.println(e-s);
}
catch
(UnknownHostException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
catch
(ExecutionException e) {
e.printStackTrace();
}
}
}
|
3.2 NIO性能测试入口
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
|
package
com.example.nio;
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.io.InputStreamReader;
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.nio.charset.Charset;
import
java.util.Scanner;
import
java.util.concurrent.Callable;
import
java.util.concurrent.ExecutionException;
import
java.util.concurrent.Executors;
import
java.util.concurrent.Future;
/**
* 客户端
* @author arthur
*/
public
class
BenchMarkNClient {
private
Selector selector;
private
Charset charset = Charset.forName(
"UTF-8"
);
private
SocketChannel sc =
null
;
public
void
init()
throws
IOException {
long
s = System.currentTimeMillis();
selector = Selector.open();
InetSocketAddress isa =
new
InetSocketAddress(
"127.0.0.1"
,
3000
);
for
(
int
i =
0
; i <
10000
; i++) {
sc = SocketChannel.open(isa);
sc.configureBlocking(
false
);
sc.register(selector, SelectionKey.OP_READ);
Future<String> future = Executors.newFixedThreadPool(
4
).submit(
new
ClientTask());
try
{
System.out.println(i+
":"
+future.get());
}
catch
(InterruptedException e) {
e.printStackTrace();
}
catch
(ExecutionException e) {
e.printStackTrace();
}
}
long
e= System.currentTimeMillis();
System.out.println(e-s);
}
private
class
ClientTask
implements
Callable<String> {
public
String call() {
try
{
while
(selector.select() >
0
) {
for
(SelectionKey sk : selector.selectedKeys()) {
selector.selectedKeys().remove(sk);
if
(sk.isReadable()) {
SocketChannel sc = (SocketChannel)sk.channel();
ByteBuffer buff = ByteBuffer.allocate(
1024
);
String content =
""
;
while
(sc.read(buff) >
0
) {
sc.read(buff);
buff.flip();
content += charset.decode(buff);
buff.clear();
}
sk.interestOps(SelectionKey.OP_READ);
return
content;
}
}
}
}
catch
(IOException e) {
e.printStackTrace();
}
return
null
;
}
}
public
static
void
main(String[] args)
throws
IOException {
new
BenchMarkNClient().init();
}
}
|
3.3 性能对比
次数 |
NIO |
SOCKET(ms) |
1000 |
525 |
637 |
2000 |
1411 | 1215 |
2000(休眠时间为100毫秒) |
205928 | 206313 |
5000 |
6731 |
2976 |
次数较少时,NIO性能较好。但随着次数增加,性能下降非常厉害。(存疑)
当通讯时间变长时,发现NIO性能又相对提高了。
可见一个技术的好坏,是和业务场景分不开的。
本文转自 randy_shandong 51CTO博客,原文链接:http://blog.51cto.com/dba10g/1843410,如需转载请自行联系原作者