工欲善其事,必先利其器,从互联网诞生到现在,基本上所有的程序都是网络程序,很少有单机版的程序了。 而网络编程的本质是两个设备之间的数据交换,当然,在计算机网络中,设备主要指计算机。我们现在进行网络编程,基本上都是使用已经封装好的框架,毕竟自己实现维护一套网络编程框架,费时费力不说,做出来之后还不一定好用,有那时间多保养保养头发。

Socket简介

记得大学学习java的时候,并没有接触网络编程相关的基础知识,一直都是单机版程序,没有见识到网络编程的美妙,所以对socket这个东西既熟悉又陌生。熟悉的原因是在MFC实验课中接触到socket知识的,没错,就是那门古老的开发语言,现在已经销声匿迹了,陌生的原因大概也就不言而喻了。好了,说了那么多,那socket到底是什么呢?

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口 。很多同学会把tcp、udp协议和socket搞混,其实Socket只是一种连接模式,不是协议。tcp、udp是两个最基本的协议,很多其它协议都是基于这两个协议。

用socket可以创建tcp连接,也可以创建udp连接,这意味着,用socket可以创建任何协议的连接。简单的来说,socket相当于一艘船,你把目的地(ip+端口)告诉它,把要运输的货物搬上去,然后它将货物送往目的地。这个货物具体需要怎么运输,运输完成之后是否还要通知你已经到达目的地,这就是实现协议的区别了。

Socket使用

首先来回顾一下socket的基本使用,创建一个服务端所需步骤

  1. 创建ServerSocket对象绑定监听端口。
  2. 通过accept()方法监听客户端的请求。
  3. 建立连接后,通过输入输出流读取客户端发送的请求信息。
  4. 通过输出流向客户端发送请求信息。
  5. 关闭相关资源。

嗯。。。Talk is cheap,Show me the code:

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
public class SocketServer {
public static void main(String[] args) {
//第一步
SocketServer socketServer = new SocketServer();
socketServer.startServer(2333);
}
private void startServer(int port) {
try {
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("服务器已启动,等待客户连接...");
//第二步 调用accept()方法开始监听,等待客户端的连接 这个方法会阻塞当前线程
Socket socket = serverSocket.accept();
System.out.println("客户端连接成功");
//第三步 建立输入输出流读数据
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String receivedMsg;
while ((receivedMsg = bufferedReader.readLine()) != null && !("end").equals(receivedMsg)) {
System.out.println("客户端:" + receivedMsg);
//第四步 给客户端发送请求
String response = "hello client";
System.out.println("我(服务端):" + response);
bufferedWriter.write(response+ "\n");
bufferedWriter.flush();
}
//关闭相关资源
socket.close();
serverSocket.close();
bufferedWriter.close();
bufferedReader.close();

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

创建一个客户端所需步骤,其实和服务端代码差不多:

  1. 创建Socket对象,指明需要连接的服务器的地址和端口。
  2. 建立连接后,通过输出流向服务器发送请求信息。
  3. 通过输入流获取服务器的响应信息。
  4. 关闭相关资源
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
public class SocketClient {
public static void main(String[] args){
SocketClient socketClient = new SocketClient();
socketClient.startClient(2333);
}
void startClient(int port){
try {
Socket clientSocket = new Socket("localhost",port);
System.out.println("客户端已启动");
//给服务器发消息
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
//接收服务器传过来的消息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//键盘输入消息 发送给服务端
BufferedReader inputReader = new BufferedReader(new InputStreamReader(System.in));
String readLine = null;
while (!(readLine = inputReader.readLine()).equals("bye")){
System.out.println("我(客户端):" + readLine);
//将键盘输入的消息发送给服务器
bufferedWriter.write(readLine+"\n");
bufferedWriter.flush();
String response = bufferedReader.readLine();
System.out.println("服务端: " + response);
}
bufferedWriter.close();
inputReader.close();
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}

}
}

以上对于socket的示例比较简单,只实现了客户端给服务器发送消息,服务器收到消息并回复客户端。现实的情况其实很复杂,如服务器高并发的处理,客户端怎么监听服务器随时发来的消息,客户端断线重连机制,心跳包的处理,还有在对消息的拆包、粘包的处理等等。而引入mina框架,我们不必关注复杂的网络通信的实现,只需专注于具体的业务逻辑。

Mina框架简介

MINA框架是对java的NIO包的一个封装,简化了NIO程序开发的难度,封装了很多底层的细节,让开发者把精力集中到业务逻辑上来。可能有一些同学不知道NIO是什么,这里简单介绍一下,NIO就是new IO,是jdk1.4引入的,它是同步非阻塞的,比如一个服务器多个客户端,客户端发送的请求都会注册到服务器的多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。它适用于连接数目多且连接比较短的架构,比如聊天服务器。

mina简单使用

Mina 的API 将真正的网络通信与我们的应用程序隔离开来,你只需要关心你要发送、接收的数据以及你的业务逻辑即可。

先看看我们实现的效果图(由于是视频转gif,可能不太好看出来,就是一个简单的文本通讯):

serverclient

创建服务端

前面说mina将网络通信与我们的应用程序隔离开来,那我们看看怎么样实现一个TCP的服务端,最近在学习kotlin,以后的代码应该都用kotlin展示了:

1
2
3
4
5
6
7
8
9
10
//创建一个非阻塞的service端的socket
val acceptor = NioSocketAcceptor()
//设置编解码器 ProtocolCodecFilter拦截器 网络传输需要将对象转换为字节流
acceptor.filterChain.addLast("codec",ProtocolCodecFilter(TextLineCodecFactory()))
//设置读取数据的缓冲区大小
acceptor.sessionConfig.readBufferSize = 2048
//读写通道10秒内无操作进入空闲状态
acceptor.sessionConfig.setIdleTime(IdleStatus.BOTH_IDLE, 10)
//绑定端口
acceptor.bind(InetSocketAddress(8080))

这段代码我们就初始化了一个TCP服务端,其中,编解码器使用的是mina自带的换行符编解码器工厂,设置编解码器是因为在网络上传输数据时,发送端发送数据需要将对象转换为字节流进行传输,接收端收到数据后再将字节流转换回来。相当于双方约定一套规则,具体规则可以自己定,也可以用现成的。我这里只需要发送文本,就用内置的啦。

网络通信已经实现,那发送、接收数据呢?我们具体的业务逻辑都在IoHandler这个类中进行处理,编写一个类继承IoHandlerAdapter ,并重写它的几个方法,记得在bind端口之前调用acceptor.setHandler(MyIoHandlerAdapter()),不然无法监听到具体事件 :

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
/**
* 向客户端发送消息后会调用此方法
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageSent(session: IoSession?, message: Any?) {
super.messageSent(session, message)
LogUtils.i("服务器发送消息成功")
}

/**
* 从端口接受消息,会响应此方法来对消息进行处理
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageReceived(session: IoSession?, message: Any?) {
super.messageReceived(session, message)
val msg = message!!.toString()
LogUtils.i("服务器接收消息成功:$msg")
}

/**
* 服务器与客户端创建连接
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionCreated(session: IoSession?) {
super.sessionCreated(session)
LogUtils.i("服务器与客户端创建连接")
}

/**
* 服务器与客户端连接打开
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionOpened(session: IoSession?) {
super.sessionOpened(session)
LogUtils.i("服务器与客户端连接打开")
}

/**
* 关闭与客户端的连接时会调用此方法
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionClosed(session: IoSession?) {
super.sessionClosed(session)
LogUtils.i("关闭与客户端的连接时会调用此方法")
}

/**
* 服务器进入空闲状态
* @param session
* @param status
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
super.sessionIdle(session, status)
LogUtils.i("服务器进入空闲状态")
}

/**
* 异常
* @param session
* @param cause
* @throws Exception
*/
@Throws(Exception::class)
override fun exceptionCaught(session: IoSession?, cause: Throwable) {
super.exceptionCaught(session, cause)
LogUtils.i("服务器异常$cause")
}

大家应该注意到IoSession这个东西了,每一个方法参数里都有它,那它具体是干什么的呢?

IoSession是一个接口,这个接口用于表示Server 端与Client 端的连接,IoAcceptor.accept()的时候返回实例。这个接口有如下常用的方法:

  1. WriteFuture write(Object message):这个方法用于写数据,该操作是异步的。
  2. CloseFuture close(boolean immediately):这个方法用于关闭IoSession,该操作也是异步的,参数指定true 表示立即关闭,否则就在所有的写操作都flush 之后再关闭。
  3. Object setAttribute(Object key,Object value):这个方法用于给我们向会话中添加一些属性,这样可以在会话过程中都可以使用,类似于HttpSession 的setAttrbute()方法。IoSession 内部使用同步的HashMap 存储你添加的自定义属性。
  4. SocketAddress getRemoteAddress():这个方法获取远端连接的套接字地址。
  5. void suspendWrite():这个方法用于挂起写操作,那么有void resumeWrite()方法与之配对。对于read()方法同样适用。
  6. ReadFuture read():这个方法用于读取数据, 但默认是不能使用的, 你需要调用IoSessionConfig 的setUseReadOperation(true)才可以使用这个异步读取的方法。一般我们不会用到这个方法,因为这个方法的内部实现是将数据保存到一个BlockingQueue,假如是Server 端,因为大量的Client 端发送的数据在Server 端都这么读取,那么可能会导致内存泄漏,但对于Client,可能有的时候会比较便利。
  7. IoService getService():这个方法返回与当前会话对象关联的IoService 实例。

换言之,拿到IoSession就能够进行两端打call了,什么?你问我怎么打call?

1
2
3
4
5
6
fun sendText(message: String,client IoSession){
var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
ioBuffer.put(message.toByteArray())
ioBuffer.flip()
client.write(ioBuffer)
}
创建客户端

无论是Server 端还是Client 端,在Mina中的执行流程都是一样的。唯一不同的就是IoService 的Client 端实现是IoConnector。

1
2
3
4
5
6
7
8
9
val connector = NioSocketConnector()
// 设置链接超时时间
connector.connectTimeoutMillis = 15000
// 添加过滤器
connector.filterChain.addLast("codec",ProtocolCodecFilter(TextLineCodecFactory()))
connector.setDefaultRemoteAddress(InetSocketAddress(ip, port))
val future = connector.connect()
future.awaitUninterruptibly()// 等待连接创建完成
var session = future.session// 获得IoSession

IoHandlerAdapter 和服务端一样,这里不做过多介绍。

最后贴上服务端代码:

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
class MinaServer : IoHandlerAdapter(){
private val acceptor: NioSocketAcceptor
private var isConnected = false
private var handler: Handler by Delegates.notNull()
init {
//创建一个非阻塞的service端的socket
acceptor = NioSocketAcceptor()
//设置编解码器 ProtocolCodecFilter拦截器 网络传输需要将对象转换为字节流
acceptor.filterChain.addLast("codec",
ProtocolCodecFilter(TextLineCodecFactory()))
//设置读取数据的缓冲区大小
acceptor.sessionConfig.readBufferSize = 2048
//读写通道10秒内无操作进入空闲状态
acceptor.sessionConfig.setIdleTime(IdleStatus.BOTH_IDLE, 10)
handler = Handler()

}

fun connect(port: Int): MinaServer {
if (isConnected)
return this
thread {
try {
//注册回调 监听和客户端之间的消息
acceptor.handler = this
acceptor.isReuseAddress = true
//绑定端口
acceptor.bind(InetSocketAddress(port))
isConnected = true
handler.post {
connectCallback?.onOpened()
}
} catch (e: Exception) {
e.printStackTrace()
handler.post {
connectCallback?.onError(e)
}
LogUtils.i("服务器连接异常")
isConnected = false
}
}
return this
}

fun sendText(message: String){
for (client in acceptor.managedSessions.values){
var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
ioBuffer.put(message.toByteArray())
ioBuffer.flip()
client.write(ioBuffer)
}
}

/**
* 向客户端发送消息后会调用此方法
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageSent(session: IoSession?, message: Any?) {
super.messageSent(session, message)
LogUtils.i("服务器发送消息成功")
connectCallback?.onSendSuccess()
}

/**
* 从端口接受消息,会响应此方法来对消息进行处理
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageReceived(session: IoSession?, message: Any?) {
super.messageReceived(session, message)
handler.post {
connectCallback?.onGetMessage(message)
}
val msg = message!!.toString()
LogUtils.i("服务器接收消息成功:$msg")
}

/**
* 服务器与客户端创建连接
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionCreated(session: IoSession?) {
super.sessionCreated(session)
handler.post {
connectCallback?.onConnected()
}
LogUtils.i("服务器与客户端创建连接")
}

/**
* 服务器与客户端连接打开
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionOpened(session: IoSession?) {
super.sessionOpened(session)
LogUtils.i("服务器与客户端连接打开")
}

/**
* 关闭与客户端的连接时会调用此方法
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionClosed(session: IoSession?) {
super.sessionClosed(session)
handler.post {
connectCallback?.onDisConnected()
}
LogUtils.i("关闭与客户端的连接时会调用此方法")
}

/**
* 服务器进入空闲状态
* @param session
* @param status
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
super.sessionIdle(session, status)
LogUtils.i("服务器进入空闲状态")
}

/**
* 异常
* @param session
* @param cause
* @throws Exception
*/
@Throws(Exception::class)
override fun exceptionCaught(session: IoSession?, cause: Throwable) {
super.exceptionCaught(session, cause)
handler.post {
connectCallback?.onError(cause)
}
LogUtils.i("服务器异常$cause")
}

private var connectCallback:ConnectCallback? = null
fun setConnectCallback(callback:ConnectCallback){
this.connectCallback = callback
}
interface ConnectCallback{
fun onSendSuccess()
fun onGetMessage(message: Any?)
fun onOpened()
fun onConnected()
fun onDisConnected()
fun onError(cause: Throwable)
}

}

在服务端中的activity中使用:

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
var mServer = MinaServer()
mServer
.connect(2333)
.setConnectCallback(object : MinaServer.ConnectCallback {
override fun onSendSuccess() {
//发送消息成功
}

override fun onGetMessage(message: Any?) {
//接收消息成功
val msg = message.toString()
}

override fun onOpened() {

}
override fun onConnected() {

}

override fun onDisConnected() {

}

override fun onError(cause: Throwable) {
Toast.makeText(applicationContext, "服务器异常" + cause.toString(), Toast.LENGTH_SHORT).show()
}

})

再看客户端代码:

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
lass MinaClient : IoHandlerAdapter(){
private val connector: NioSocketConnector
private var session: IoSession? = null

var isConnected = false
private var handler:Handler by Delegates.notNull()
init {
connector = NioSocketConnector()
// 设置链接超时时间
connector.connectTimeoutMillis = 15000
// 添加过滤器
connector.filterChain.addLast("codec",
ProtocolCodecFilter(TextLineCodecFactory()))
handler = Handler()
}

fun connect(ip: String, port: Int): MinaClient {
if (isConnected)
return this
thread {
connector.handler = this
connector.setDefaultRemoteAddress(InetSocketAddress(ip, port))
//开始连接
try {
val future = connector.connect()
future.awaitUninterruptibly()// 等待连接创建完成
session = future.session// 获得session
isConnected = session != null && session!!.isConnected
} catch (e: Exception) {
e.printStackTrace()
handler.post {
connectCallback?.onError(e)
}
println("客户端链接异常...")
}
}
return this
}
fun disConnect(){
if (isConnected){
session?.closeOnFlush()
connector.dispose()
}else{
connectCallback?.onDisConnected()
}
}

fun sendText(message: String){
var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
ioBuffer.put(message.toByteArray())
ioBuffer.flip()
session?.write(ioBuffer)
}
/**
* 向服务端端发送消息后会调用此方法
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageSent(session: IoSession?, message: Any?) {
super.messageSent(session, message)
LogUtils.i("客户端发送消息成功")
handler.post {
connectCallback?.onSendSuccess()
}
}

/**
* 从端口接受消息,会响应此方法来对消息进行处理
* @param session
* @param message
* @throws Exception
*/
@Throws(Exception::class)
override fun messageReceived(session: IoSession?, message: Any?) {
super.messageReceived(session, message)
LogUtils.i("客户端接收消息成功:")
handler.post {
connectCallback?.onGetMessage(message)
}
}

/**
* 服务器与客户端创建连接
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionCreated(session: IoSession?) {
super.sessionCreated(session)
LogUtils.i("服务器与客户端创建连接")
handler.post {
connectCallback?.onConnected()
}
}

/**
* 服务器与客户端连接打开
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionOpened(session: IoSession?) {
super.sessionOpened(session)
LogUtils.i("服务器与客户端连接打开")
}

/**
* 关闭与客户端的连接时会调用此方法
* @param session
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionClosed(session: IoSession?) {
super.sessionClosed(session)
LogUtils.i("关闭与客户端的连接时会调用此方法")
isConnected = false
handler.post {
connectCallback?.onDisConnected()
}
}

/**
* 客户端进入空闲状态
* @param session
* @param status
* @throws Exception
*/
@Throws(Exception::class)
override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
super.sessionIdle(session, status)
LogUtils.i("客户端进入空闲状态")
}

/**
* 异常
* @param session
* @param cause
* @throws Exception
*/
@Throws(Exception::class)
override fun exceptionCaught(session: IoSession?, cause: Throwable) {
super.exceptionCaught(session, cause)
LogUtils.i("客户端异常$cause")
handler.post {
connectCallback?.onError(cause)
}
}
private var connectCallback:ConnectCallback? = null
fun setConnectCallback(callback:ConnectCallback){
this.connectCallback = callback
}
interface ConnectCallback{
fun onSendSuccess()
fun onGetMessage(message: Any?)
fun onConnected()
fun onDisConnected()
fun onError(cause: Throwable)
}
}

客户端的activity中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var mClient = MinaClient()
mClient
.connect("192.168.0.108", 2333)
.setConnectCallback(object : MinaClient.ConnectCallback {
override fun onGetMessage(message: Any?) {
val msg = message.toString()
}
override fun onConnected() {

}

override fun onDisConnected() {
Toast.makeText(applicationContext, "断开连接成功", Toast.LENGTH_SHORT).show()
}
override fun onError(cause: Throwable) {
Toast.makeText(applicationContext, "服务器异常" + cause.toString(), Toast.LENGTH_SHORT).show()
}

override fun onSendSuccess() {

}

})

界面布局比较简单,就是一个recyclerview+几个button,如果觉得我讲得不够清楚T^T,可以到github上查看源码:minaSimple