Android 基于TCP的 Socket 编程实现(结合 okio)

2022-09-06 17:13:24 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

前言

两个进程如果要进行通讯最基本的一个前提就是能够唯一的标识一个进程,在本地进程通讯中我们可以使用 PID 来唯一标识一个进程,但 PID 只在本地是唯一的,网络中两个进程 PID 冲突几率很大,这时我们就需要通过其他手段来唯一标识网络中的进程了,我们知道 IP 层的 ip 地址可以唯一标示主机,而 TCP 层协议和端口号结合就可以唯一标示主机的一个进程了。

能够唯一标示网络中的进程后,它们就可以利用 Socket 进行通信了,什么是 Socket 呢?我们经常把 Socket 翻译为套接字(为什么翻译成套接字),Socket 是在应用层和传输层之间的一个抽象层,它把 TCP/IP 层复杂的操作抽象为几个简单的接口供应用层调用,从而实现进程在网络中通信。

相关类

这里提到的 Socket 为广义上的 Socket 编程,它可以基于 TCP 或者 UDP 实现,Java 为 Socket 编程封装了几个重要的类,如下:

  • Socket (TCP)

Socket 类实现了一个客户端 Socket,作为两台机器通信的终端,默认采用的传输层协议为 TCP 可靠传输协议。Socket 类除了构造函数返回一个 socket 外,还提供了 connect , getOutputStream, getInputStream 和 close 方法。connect 方法用于请求一个 socket 连接,getOutputStream 用于获得写 socket的输出流,getInputStream 用于获得读 socket 的输入流,close 方法用于关闭一个流。

  • DatagramSocket (UDP)

DatagramSocket 类实现了一个发送和接收数据报的 socket,传输层协议使用 UDP,不能保证数据报的可靠传输。DataGramSocket 主要有 send, receive 和 close 三个方法。send 用于发送一个数据报,Java 提供了 DatagramPacket 对象用来表达一个数据报。receive 用于接收一个数据报,调用该方法后,一直阻塞接收到直到数据报或者超时。close 是关闭一个 socket。

  • ServerSocket

ServerSocket 类实现了一个服务器 socket,一个服务器 socke t等待客户端网络请求,然后基于这些请求执行操作,并返回给请求者一个结果。ServerSocket 提供了 bind、accept 和 close 三个方法。bind 方法为ServerSocket 绑定一个IP地址和端口,并开始监听该端口。accept 方法为 ServerSocket 接受请求并返回一个 Socket 对象,accept 方法调用后,将一直阻塞直到有请求到达。close 方法关闭一个 ServerSocket 对象。

  • SocketAddress

SocketAddress 提供了一个 socket 地址,不关心传输层协议。这是一个虚类,由子类来具体实现功能、绑定传输协议。它提供了一个不可变的对象,被 socket 用来绑定、连接或者返回数值。

  • InetSocketAddress

InetSocketAddress 实现了IP地址的 SocketAddress,也就是有 IP 地址和端口号表达 Socket 地址。如果不制定具体的 IP 地址和端口号,那么 IP 地址默认为本机地址,端口号随机选择一个。

  • DatagramPacket(UDP)

DatagramSocket 是面向数据报 socket 通信的一个可选通道。数据报通道不是对网络数据报 socket 通信的完全抽象。socket通信的控制由DatagramSocket 对象实现。DatagramPacket 需要与 DatagramSocket 配合使用才能完成基于数据报的 socket 通信。

基于TCP的 Socket

基于 TCP 的 Socket可以实现客户端—服务器间的双向实时通信。上面提到的 java.net 包中定义的两个类 Socket 和 ServerSocket,分别用来实现双向连接的 client 和 server 端。

具体的实现步骤在我另外一篇循序渐进Socket网络编程(多客户端、信息共享、文件传输)中有很详细的描述,接下来看 Android 端如何实现基于 TCP 的 Socket 连接。

实现

客户端连接:

服务端连接:

客户端发送:

服务端接收:

服务端代码:

代码语言:javascript复制
public class SocketTest {

	private static final int PORT = 9999;
	private List<Socket> mList = new ArrayList<Socket>();
	private ServerSocket server = null;
	private ExecutorService mExecutorService = null;
	private String receiveMsg;
	private String sendMsg;

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

	public SocketTest() {
		try {
			server = new ServerSocket(PORT);
			mExecutorService = Executors.newCachedThreadPool();
			System.out.println("服务器已启动...");
			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 PrintWriter printWriter=null;

		public Service(Socket socket) {
			this.socket = socket;
			try {
				printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter( socket.getOutputStream(), "UTF-8")), true);
				in = new BufferedReader(new InputStreamReader(
						socket.getInputStream(),"UTF-8"));
				printWriter.println("成功连接服务器" "(服务器发送)");
				System.out.println("成功连接服务器");
			} catch (IOException e) {
				e.printStackTrace();
			}

		}

		@Override
		public void run() {
			try {
				while (true) {
					if ((receiveMsg = in.readLine())!=null) {
						System.out.println("receiveMsg:" receiveMsg);
						if (receiveMsg.equals("0")) {
							System.out.println("客户端请求断开连接");
							printWriter.println("服务端断开连接" "(服务器发送)");
							mList.remove(socket);
							in.close();
							socket.close();
							break;
						} else {
							sendMsg = "我已接收:"   receiveMsg   "(服务器发送)";
							printWriter.println(sendMsg);
						}
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

服务端使用线程池实现多客户端连接,server.accept() 表示等待客户端连接,当有客户端连接时新建一个线程去处理,其中涉及到的方法之前都提到过,不再赘述。

客户端代码:

代码语言:javascript复制
public class SocketActivity extends AppCompatActivity {

    private EditText mEditText;
    private TextView mTextView;
    private static final String TAG = "TAG";
    private static final String HOST = "192.168.23.1";
    private static final int PORT = 9999;
    private PrintWriter printWriter;
    private BufferedReader in;
    private ExecutorService mExecutorService = null;
    private String receiveMsg;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_socket);
        mEditText = (EditText) findViewById(R.id.editText);
        mTextView = (TextView) findViewById(R.id.textView);
        mExecutorService = Executors.newCachedThreadPool();
    }

    public void connect(View view) {
        mExecutorService.execute(new connectService());
    }
    
    public void send(View view) {
        String sendMsg = mEditText.getText().toString();
        mExecutorService.execute(new sendService(sendMsg));
    }

    public void disconnect(View view) {
        mExecutorService.execute(new sendService("0"));
    }

    private class sendService implements Runnable {
        private String msg;

        sendService(String msg) {
            this.msg = msg;
        }

        @Override
        public void run() {
            printWriter.println(this.msg);
        }
    }

    private class connectService implements Runnable {
        @Override
        public void run() {
            try {
                Socket socket = new Socket(HOST, PORT);
                socket.setSoTimeout(60000);
                printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
                        socket.getOutputStream(), "UTF-8")), true);
                in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
                receiveMsg();
            } catch (Exception e) {
                Log.e(TAG, ("connectService:"   e.getMessage()));
            }
        }
    }
    
    private void receiveMsg() {
        try {
            while (true) {
                if ((receiveMsg = in.readLine()) != null) {
                    Log.d(TAG, "receiveMsg:"   receiveMsg);
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTextView.setText(receiveMsg   "nn"   mTextView.getText());
                        }
                    });
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "receiveMsg: ");
            e.printStackTrace();
        }
    }
}

客户端同样使用了线程池进行管理,把连接和发送分割为两个 Runnable 易于调用,当发送 “0” 且服务端收到时关闭连接。

okio 实现

到这里一个简单的 Socket 通信就完成了,其中对于 Socket 的信息流使用的是 java.io,之前学习 okio 时,了解到 okio 可以替代 java.io,okio是一个由square公司开发的开源库,它弥补了Java.io和java.nio的不足,能够更方便快速的读取、存储和处理数据(了解更多请点击 Okio源码分析),下面就尝试用 okio 替换 java.io。

直接上代码:

服务端代码:

代码语言:javascript复制
public class SocketTest {

	private static final int PORT = 9999;
	private List<Socket> mList = new ArrayList<Socket>();
	private ServerSocket server = null;
	private ExecutorService mExecutorService = null;
	private String receiveMsg;
	private String sendMsg;

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

	public SocketTest() {
		try {
			server = new ServerSocket(PORT);
			mExecutorService = Executors.newCachedThreadPool();
			System.out.println("服务器已启动...");
			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 BufferedSink mSink;
		private BufferedSource mSource;

		public Service(Socket socket) {
			this.socket = socket;
			try {
				mSink = Okio.buffer(Okio.sink(socket));
				mSource = Okio.buffer(Okio.source(socket));
				sendMsg="成功连接服务器"   "(服务器发送)";
				mSink.writeUtf8(sendMsg "n");
				mSink.flush();
				System.out.println("成功连接服务器");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		@Override
		public void run() {
			try {
				while (true) {
					for (String receiveMsg; (receiveMsg = mSource
							.readUtf8Line()) != null;) {
						System.out.println("receiveMsg:"   receiveMsg);
						if (receiveMsg.equals("0")) {
							System.out.println("客户端请求断开连接");
							mSink.writeUtf8("服务端断开连接"   "(服务器发送)");
							mSink.flush();
							mList.remove(socket);
							socket.close();
							break;
						} else {
							sendMsg = "我已接收:"   receiveMsg   "(服务器发送)";
							mSink.writeUtf8(sendMsg "n");
							mSink.flush();
						}
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

客户端代码:

代码语言:javascript复制
public class SocketActivity extends AppCompatActivity {

    private EditText mEditText;
    private TextView mTextView;
    private static final String TAG = "TAG";
    private static final String HOST = "192.168.23.1";
    private static final int PORT = 9999;
    private BufferedSink mSink;
    private BufferedSource mSource;
    private ExecutorService mExecutorService = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_socket);
        mEditText = (EditText) findViewById(R.id.editText);
        mTextView = (TextView) findViewById(R.id.textView);
        mExecutorService = Executors.newCachedThreadPool();
    }

    public void connect(View view) {
        mExecutorService.execute(new connectService());
    }

    public void send(View view) {
        String sendMsg = mEditText.getText().toString();
        mExecutorService.execute(new sendService(sendMsg));
    }

    public void disconnect(View view) {
        mExecutorService.execute(new sendService("0"));
    }

    private class sendService implements Runnable {
        private String msg;

        sendService(String msg) {
            this.msg = msg;
        }

        @Override
        public void run() {
            try {
                mSink.writeUtf8(this.msg "n");
                mSink.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private class connectService implements Runnable {
        @Override
        public void run() {
            try {
                Socket socket = new Socket(HOST, PORT);
                mSink = Okio.buffer(Okio.sink(socket));
                mSource = Okio.buffer(Okio.source(socket));
                receiveMsg();
            } catch (Exception e) {
                Log.e(TAG, ("connectService:"   e.getMessage()));
            }
        }
    }

    private void receiveMsg() {
        try {
            while (true) {
                for (String receiveMsg; (receiveMsg = mSource.readUtf8Line()) != null; ) {
                    Log.d(TAG, "receiveMsg:"   receiveMsg);
                    final String finalReceiveMsg = receiveMsg;
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mTextView.setText(finalReceiveMsg   "nn"   mTextView.getText());
                        }
                    });
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "receiveMsg: ");
            e.printStackTrace();
        }
    }
}

这里有一个很坑的地方:

代码语言:javascript复制
          mSink.writeUtf8(this.msg "n");
          mSink.flush();

起初没有加 “n” 时,调用 flush 方法后消息是无法发送成功的,除非调用 sink.close 方法后才会发送成功,但是我们不能每发送一次就 close 掉,对比 printWriter.println 方法,尝试加上一个换行符,果真发送成功。

总结

Android 有两种通信方式,一种是常用的基于 HTTP 协议方式,另一种就是基于 TCP/UDP 协议的 Socket 方式。虽然大部分需求都可通过 HTTP 实现,实现起来也较为简单,但某些情景下需要使用 Socket 方式,这时永远不要放弃去使用最佳的工具来解决问题的机会。本文主要通过 Socket 实现了 Android 基于 TCP 协议的通信,后面将 Socket 的输入输出流处理由 java.io 替换为 Okio 实现,虽然说 Okio 弥补了Java.io和 java.nio 的不足,能够更方便快速的读取、存储和处理数据,但是实际性能并没测试过,这里主要是为了复习一下 Okio 的使用,另外就是在 Okio源码分析 中没有涉及到 Socket 的内容,这里正好填补一下知识漏洞。

参考文章

http://www.cnblogs.com/hongyanee/p/3288184.html

http://www.cnblogs.com/wisdo/p/5860001.html

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/134298.html原文链接:https://javaforall.cn

0 人点赞