Android Socket编程

一、什么是Socket

什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

二、Socket的连接过程


根据连接启动的方式以及本地Socket要连接的目标,Socket之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。


1、服务器监听:是服务器端Socket并不定位具体的客户端Socket,而是处于等待连接的状态,实时监控网络状态。


2、客户端请求:是指由客户端的Socket提出连接请求,要连接的目标是服务器端的Socket。为此,客户端的Socket必须首先描述它要连接的服务器的Socket,指出服务器端Socket的地址和端口号,然后就向服务器端Socket提出连接请求。


3、连接确认:是指当服务器端Socket监听到或者说接收到客户端Socket的连接请求,它就响应客户端Socket的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端Socket继续处于监听状态,继续接收其他客户端Socket的连接请求。


三、Android socket的基础知识 

1.1         Accept Timeout

Accept timeout 仅对ServerSocket有用。ServerSocket 使用accept()方法来监听客户端Socket的连接。默认,ServerSocket.accept() 方法会一直阻塞直到有客户端来连接。通常,我们不需要设置accept timeout.

 

但有时候特殊情况,还是要考虑设置accept timeout.

比如: 程序A给程序B发了一个JMS消息,然后程序A启动一个Socket Server,想通过socket等待接收程序B的返回消息。如果不设置accept timeout, 并且程序B因为某些原因一直不能连接Socket Server,最终会导致程序A挂起。

 

Accept Timeout可以这样设置:

ServerSocket serverSocket = new ServerSocket(5555);
serverSocket.setSoTimeout(5000); // in milliseconds
while (true) {
    Socket socket = serverSocket.accept();
        …
}

 

1.2         Connect Timeout

当Client端连接Server端的时候,可以指定Connect Timeout

如果没有指定,会使用操作系统的默认值:


OS

Default TCP timeout

BSD

75 seconds

Linux

189 seconds

Solaris

225 seconds

Windows XP

21 seconds


 

Connect Timeout可以这样设置:

SocketAddress socketAddress = new InetSocketAddress(host, port);
socket = new Socket();
socket.connect(socketAddress, connectTimeout);

1.3         Receive Timeout

当socket从另一方接收数据时,可以设置Receive Timeout

默认没有timeout,socket会一直阻塞直到有数据可读取。

Receive Timeout可以这样设置:

Socket socket = new Socket(host, port);
socket.setSoTimeout(timeout);

1.4         Send Timeout

Send Timeout是socket给另一方发送数据时使用的。

不过Java里没有办法设置Send Timeout.

当然,socket发送数据的时候,会首先发送到本机OS的一个buffer内。一般只要一次发送的数据不是很大,即使对方挂起或暂时不能接收数据,也不会导致发送方挂起。

 

2.1       Socket ack (acknowledgement)

Socket ack是指当socket接收到数据之后,发送一个ack字符串(比如$ACK)给socket发送方。这样,socket发送方可以根据是否收到了ack判断对方是否收到了数据。

Socket ack是显示的在应用程序中加入的一种通讯协议。如果不使用ack,在socket通讯中,可能会丢失数据。

 

比如,socket client要连续的给socket server发送100条消息。如果我们在server收到第50条消息的时候,强行kill了server。那么查询client端发送的log,可能client端成功发送了51条。只有当client端发送第52条消息的时候才遇到异常。这样第51条消息就丢失了。

所以为了确保数据传输的准确性,我们可以引入ack协议。有时我们不仅要确保server不但收到了数据,而且还要保证server成功处理了数据。这时,可以等server成功处理完数据之后,再给client发ack。

 

2.2       Socket Keep Alive

Socket连接像数据库连接一样,属于重量型资源。如果我们频繁的创建socket、发送/接收数据、关闭socket,那么会有很大一部分时间浪费在socket的创建和关闭上。

所以,如果我们经常需要与同一个socket地址发送/接收数据时,应该考虑只创建一次socket,然后一直使用这个socket对象发送/接收数据。

 

2.3       Heartbeat

通常,我们会设置socket的receive timeout。这样,如果我们一直打开着socket (keep alive), 而很长时间又没有数据通讯,socket接收方就会timeout,最终导致打开的连接坏掉。

如果很长时间没有数据通讯,防火墙或代理服务器也可能会关闭打开的socket连接。

所以为了保证打开的socket连接一直可用,如果一段时间没有数据进行通讯(或指定一个时间间隔),我们可以显示的发送一个heartbeat消息(比如: $HRT)给对方,从而保证连接不会被异常关闭。

 

2.4       Socket Close

每一个socket对象会持有一个socket descriptor (其实就是file descriptor),操作系统对于socket descriptor有一个最大限制。因此当socket不再使用时,一定要记得关闭,即使socket连接失败或出现异常,只要socket对象不为null,一定要记得关闭。

四、socket服务端的编写

服务器端编程步骤: 
1: 创建服务器端套接字并绑定到一个端口上(0-1023是系统预留的,最好大约1024以上) 
2: 套接字设置监听模式等待连接请求 
3: 接受连接请求后进行通信 
4: 返回,等待下一个连接请求 

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SckServer {
	private static final int PORT = 9999;// 端口
	private List<Socket> mList = new ArrayList<Socket>();
	private ServerSocket server = null;
	private ExecutorService mExecutorService = null; // thread pool

	public static void main(String[] args) {
		new SckServer();
	}

	public SckServer() {
		try {
			// 创建服务器端socket并绑定到一个端口上
			server = new ServerSocket(PORT);
			// 使用连接池
			mExecutorService = Executors.newCachedThreadPool();
			System.out.print("server start ...");
			Socket client = null;
			// 接字设置监听模式等待连接请求
			while (true) {
				client = server.accept();
				mList.add(client);
				// 接受连接请求后进行通信
				mExecutorService.execute(new Service(client));
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	class Service implements Runnable {
		private Socket socket;
		private BufferedReader in = null;
		private String msg = "";

		public Service(Socket socket) {
			this.socket = socket;
			try {
				in = new BufferedReader(new InputStreamReader(
						socket.getInputStream()));
				msg = "user" + this.socket.getInetAddress() + "come toal:"
						+ mList.size();
				this.sendmsg();
			} catch (IOException e) {
				e.printStackTrace();
			}

		}

		@Override
		public void run() {
			// TODO Auto-generated method stub
			try {
				while (true) {
					if ((msg = in.readLine()) != null) {
						if (msg.equals("exit")) {
							System.out.println("ssssssss");
							mList.remove(socket);
							in.close();
							msg = "user:" + socket.getInetAddress()
									+ "exit total:" + mList.size();
							socket.close();
							this.sendmsg();
							break;
						} else {
							msg = socket.getInetAddress() + ":" + msg;
							this.sendmsg();
						}
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		public void sendmsg() {
			System.out.println(msg);
			int num = mList.size();
			for (int index = 0; index < num; index++) {
				Socket mSocket = mList.get(index);
				PrintWriter pout = null;
				try {
					pout = new PrintWriter(new BufferedWriter(
							new OutputStreamWriter(mSocket.getOutputStream())),
							true);
					pout.println(msg);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

五、Android客户端的编写

客户端编程步骤: 
1: 创建客户端套接字(指定服务器端IP地址与端口号) 
2: 连接(Android 创建Sockett时会自动连接) 
3: 与服务器端进行通信 
4: 关闭套接字 

package com.lyh.sck;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends Activity {
	private TextView tv_msg = null;
	private EditText ed_msg = null;
	private Button btn_send = null;
	private static final String HOST = "192.168.3.121";
	private static final int PORT = 9999;
	private Socket socket = null;
	private BufferedReader in = null;
	private PrintWriter out = null;
	private String content = "";
	public Handler scHandler;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		initData();
		findViews();
		initSocket();
	}
	
	/**
	 * 初始化控件
	 * */
	private void findViews() {
		tv_msg = (TextView) findViewById(R.id.TextView);
		ed_msg = (EditText) findViewById(R.id.EditText01);
		btn_send = (Button) findViewById(R.id.Button02);
	}
	
	/**
	 * 初始化数据
	 * */
	private void initData() {
		scHandler = new Handler() {
			@Override
			public void handleMessage(Message msg) {
				// TODO Auto-generated method stub
				super.handleMessage(msg);
				if (msg.what == 0) {
					btn_send.setOnClickListener(new Button.OnClickListener() {
						@Override
						public void onClick(View v) {
							// TODO Auto-generated method stub
							String msg = ed_msg.getText().toString();
							if (socket.isConnected()) {
								if (!socket.isOutputShutdown()) {
									out.println(msg);
								}
							}
						}
					});
					new Thread(runnable).start();
				} else if (msg.what == 1) {
					tv_msg.setText(tv_msg.getText().toString() + content);
				} else if (msg.what == 2) {

				}
			}

		};
	}

	/**
	 * 初始化Socket
	 * */
	public void initSocket() {
		new Thread() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					socket = new Socket(HOST, PORT);//初始化socket对象
					in = new BufferedReader(new InputStreamReader(
							socket.getInputStream()));
					out = new PrintWriter(new BufferedWriter(
							new OutputStreamWriter(socket.getOutputStream())),
							true);
					//初始化玩socket,将结果告知scHandler
					Message msg=scHandler.obtainMessage();
					msg.what=0;
					scHandler.sendMessage(msg);
				} catch (IOException ex) {
					ex.printStackTrace();
					ShowDialog("login exception" + ex.getMessage());
				}
			}
		}.start();

	}
	/***
	 * 获取数据线程
	 * */
	public Runnable runnable = new Runnable() {
		@Override
		public void run() {
			// TODO Auto-generated method stub
			try {
				while (true) {
					if (socket.isConnected()) {
						if (!socket.isInputShutdown()) {
							if ((content = in.readLine()) != null) {
								content += "\n";
								Message msg=scHandler.obtainMessage();
								msg.what=1;
								scHandler.sendMessage(msg);
							} else {

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

	  

	public void ShowDialog(String msg) {
		new AlertDialog.Builder(this).setTitle("notification").setMessage(msg)
				.setPositiveButton("ok", new DialogInterface.OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						// TODO Auto-generated method stub

					}
				}).show();
	}

}

备注:Android网络编程的时候,切记不能将耗时的上传下载线程放在UI线程中。