Non-Blocking TCP Server 模擬結果:
(1) 起始狀態
(2) 啟動Server
(3) Client端連線+發送訊息
(4) Client端離線

備註:當Server端收到Client端的訊息後,會發送回應給Client端
Sample code following:
package com.example.sample_niotcpserver;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
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.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import org.apache.http.conn.util.InetAddressUtils;
import com.example.sample_niotcpserver.R.id;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class MainActivity extends Activity implements OnClickListener {
//===== Debug Member Variables =====//
private static String Class_TAG = "MainActivity";
private static String Debug_TAG = "Debug Use";
//===== UI Member Variables =====//
private Button mBtnBind, mBtnClose;
private TextView mTvHint, mTvPostMsg;
private EditText mEtPort;
//===== nioTCP Member Variables =====//
private ServerSocketChannel mServerSocketChannel = null;
private Selector mSelector = null;
private String mLocalIP = "0.0.0.0";
private int mLocalPort = 0;
private boolean isRunning = false; // Server open state.
// Record the remote socket, IP and port.
private ArrayList<Socket> mSocketList;
private ArrayList<String> mRemoteIPList;
private ArrayList<Integer>mRemotePortList;
// When the data can't write to client, need to record the answer data to wait for until it can write.
private String mAnswerTemp = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnBind = (Button) this.findViewById(R.id.xml_btn_bind);
mBtnBind.setOnClickListener(this);
mBtnBind.setEnabled(true);
mBtnClose = (Button) this.findViewById(R.id.xml_btn_close);
mBtnClose.setOnClickListener(this);
mBtnClose.setEnabled(false);
mTvHint = (TextView) this.findViewById(R.id.xml_tv_hint);
mTvPostMsg = (TextView) this.findViewById(R.id.xml_tv_post_msg);
mEtPort = (EditText) this.findViewById(R.id.xml_et_port);
mEtPort.setText("8000");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
// The button listener method.
@Override
public void onClick(View v) {
switch (v.getId()){
case id.xml_btn_bind:
// Close the bind button.
mBtnBind.setEnabled(false);
mTvPostMsg.setText("");
mSocketList = new ArrayList<Socket>();
mSocketList.clear();
mRemoteIPList = new ArrayList<String>();
mRemoteIPList.clear();
mRemotePortList = new ArrayList<Integer>();
mRemotePortList.clear();
mLocalIP = getLocalIP();
mLocalPort = Integer.valueOf(mEtPort.getText().toString());
mTvHint.setText("Starting server...");
new Thread(nioTCPSetverThread).start();
break;
case id.xml_btn_close:
// Close the close button.
mBtnClose.setEnabled(false);
mTvHint.setText("Closing server...");
// Change server state.
isRunning = false;
mSelector.wakeup();
break;
}
}
private Runnable nioTCPSetverThread = new Runnable(){
@Override
public void run() {
try {
// Open the seletor.
mSelector = Selector.open();
try {
// Open the local TCP server socket channel.
mServerSocketChannel = ServerSocketChannel.open();
// Configure blocking mode: true(blocking) false(non-blocking).
mServerSocketChannel.configureBlocking(false);
// Bind the server with lacal IP address and port.
mServerSocketChannel.socket().bind(new InetSocketAddress(mLocalIP, mLocalPort));
// Server socket channel registers OP_ACCEPT command to server selector, and generate a SelectionKey.
mServerSocketChannel.register(mSelector, SelectionKey.OP_ACCEPT);
// Change the button state.
runOnUiThread(new Runnable(){
@Override
public void run() {
mBtnClose.setEnabled(true);
mTvHint.setText("Server bind. IP: "+ mLocalIP + " Port: "+ mLocalPort);
}});
// Change server state.
isRunning = true;
try {
while(isRunning == true) {
// Selector try to find the channel which has registered events.
mSelector.select();
Set<SelectionKey> ServerChannelKeys = mSelector.selectedKeys();
Iterator<SelectionKey> keyIter = ServerChannelKeys.iterator();
while (keyIter.hasNext() && (mSelector.isOpen() == true)) {// If keyIter has any event, return true, or retun false.
// Extract the event into key from keyIter.
SelectionKey key = (SelectionKey) keyIter.next();
// Remove keyIter when the key takes the event.
keyIter.remove();
if (key.isAcceptable()) {// Accept connect.
// Accept the client socket from the server channel.
final SocketChannel ClientSocketChannel = ((ServerSocketChannel) key.channel()).accept();
// Configure blocking mode: true(blocking) false(non-blocking).
ClientSocketChannel.configureBlocking(false);
// Client socket channel registers "OP_READ" command to server selector.
ClientSocketChannel.register(mSelector, SelectionKey.OP_READ);
// Add the connectable socket to socket list.
if (mSocketList.contains(ClientSocketChannel.socket()) == false) {
mSocketList.add(ClientSocketChannel.socket());
int socketIndex = mSocketList.indexOf(ClientSocketChannel.socket());
mRemoteIPList.add(socketIndex, ClientSocketChannel.socket().getInetAddress().toString());
mRemotePortList.add(socketIndex, ClientSocketChannel.socket().getPort());
}
// Post accept message to UI msg TextView.
runOnUiThread(new Runnable(){
@Override
public void run() {
mTvPostMsg.append("IP:"+ ClientSocketChannel.socket().getInetAddress().toString() +
"(" + ClientSocketChannel.socket().getPort() + ")" + "is online.\r\n");
}});
}// if (key.isAcceptable())
else if (key.isReadable()) {// Receive data.
ByteBuffer readBuffer = ByteBuffer.allocate(10 * 2048);
final SocketChannel ClientSocketChannel = (SocketChannel) key.channel();
int readBuffLen = ClientSocketChannel.read(readBuffer);
if (readBuffLen < 0) {// Client side socket has disconnected.
// Remove the disconnectable socket from socket list.
if (mSocketList.contains(ClientSocketChannel.socket()) == true) {
final int socketIndex = mSocketList.indexOf(ClientSocketChannel.socket());
// Post close message to UI msg TextView.
runOnUiThread(new Runnable(){
@Override
public void run() {
mTvPostMsg.append("IP:"+ mRemoteIPList.get(socketIndex) +
"(" + mRemotePortList.get(socketIndex) + ")" + "is offline.\r\n");
mRemoteIPList.remove(socketIndex);
mRemotePortList.remove(socketIndex);
}});
mSocketList.remove(ClientSocketChannel.socket());
}
// Close the channel.
key.cancel();
key.channel().close();
} else {
// Update the index of "position" and "limit", change position to "0"; limit to "valid data length".
readBuffer.flip();
int ValidDataLen = readBuffer.limit();
final byte[] ReceiveData = new byte[ValidDataLen];
readBuffer.get(ReceiveData);
// Post read message to UI msg TextView.
runOnUiThread(new Runnable(){
@Override
public void run() {
String msg = new String(ReceiveData);
mTvPostMsg.append("IP:"+ ClientSocketChannel.socket().getInetAddress().toString() +
"(" + ClientSocketChannel.socket().getPort() + ")" + "say: " + msg + "\r\n");
}});
// Try to answer the data to client.
String answer = "Server say: your message: \""+
(new String(ReceiveData)) +
"\"has received.\r\n";
ByteBuffer writeBuffer = ByteBuffer.wrap(answer.getBytes());
int writeLength = ((SocketChannel) key.channel()).write(writeBuffer);;
if (writeLength == 0) {// The channel buffer is full.
// Record the answer data.
mAnswerTemp = answer;
// Register the OP_WRITE command.
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}
}
} // else if (key.isReadable())
else if (key.isWritable()) {
ByteBuffer writeBuffer = ByteBuffer.wrap(mAnswerTemp.getBytes());
((SocketChannel) key.channel()).write(writeBuffer);
mAnswerTemp = null;
// Cancel the OP_WRITE command.
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
} // else if (key.isWritable())
}// while (keyIter.hasNext() && (mSelector.isOpen() == true))
}// while(isRunning == true)
} catch (IOException e) {
e.printStackTrace();
Log.v(Class_TAG, "nioTCPSetverThread");
Log.e(Debug_TAG, "The selecter selects fail.");
}
} catch (IOException e) {
e.printStackTrace();
Log.v(Class_TAG, "nioTCPSetverThread");
Log.e(Debug_TAG, "Server socket channel \"open\" or \"bind\" fail.");
}
} catch (IOException e) {
e.printStackTrace();
Log.v(Class_TAG, "nioTCPSetverThread");
Log.e(Debug_TAG, "Selector open fail.");
}
try {
// Close server channel and selector.
mServerSocketChannel.keyFor(mSelector).cancel();
mServerSocketChannel.socket().close();
mServerSocketChannel.close();
mServerSocketChannel = null;
mSelector.close();
mSelector = null;
// Change the button state.
runOnUiThread(new Runnable(){
@Override
public void run() {
mBtnBind.setEnabled(true);
mTvHint.setText("Server close.");
}});
} catch (IOException e) {
e.printStackTrace();
Log.v(Class_TAG, "nioTCPSetverThread");
Log.e(Debug_TAG, "Close server socket channel or selector fail.");
}
}
};
private String getLocalIP() {
try {
String ipv4;
ArrayList<NetworkInterface> nilist = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface ni: nilist)
{
ArrayList<InetAddress> ialist = Collections.list(ni.getInetAddresses());
for (InetAddress address: ialist){
if (!address.isLoopbackAddress() && InetAddressUtils.isIPv4Address(ipv4=address.getHostAddress()))
{
return ipv4;
}
}
}
} catch (SocketException ex) {
Log.v(Class_TAG, "getLocalIP");
Log.e(Debug_TAG, "Error: get local IP address.");
}
return null;
}
}
activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:id="@+id/xml_tv_hint"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:hint="Hint"
android:textColor="#000000"
android:textSize="30sp" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Port number:"
android:textColor="#000000"
android:textSize="20sp" />
<EditText
android:id="@+id/xml_et_port"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:hint="Enter the number 0~65535"
android:textSize="20sp" >
<requestFocus />
</EditText>
<Button
android:id="@+id/xml_btn_bind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.54"
android:text="Bind" />
<Button
android:id="@+id/xml_btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.42"
android:text="Close" />
</LinearLayout>
<TextView
android:id="@+id/xml_tv_post_msg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="20dp" />
</LinearLayout>
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.sample_niotcpserver"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.sample_niotcpserver.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>