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

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

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

前言

两个进程如果要进行通讯最基本的一个前提就是能够唯一的标识一个进程,在本地进程通讯中我们可以使用 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 连接。

实现

客户端连接:
这里写图片描述

服务端连接:
这里写图片描述

客户端发送:
这里写图片描述

服务端接收:
这里写图片描述

服务端代码:

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() 表示等待客户端连接,当有客户端连接时新建一个线程去处理,其中涉及到的方法之前都提到过,不再赘述。

客户端代码:

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 + "\n\n" + 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。

直接上代码:

服务端代码:

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();
			}
		}
	}
}

客户端代码:

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 + "\n\n" + mTextView.getText());
                        }
                    });
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "receiveMsg: ");
            e.printStackTrace();
        }
    }
}

这里有一个很坑的地方:

          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

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


关注公众号,Get 更多知识点

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

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

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 2021 Java面试真题集锦

    2021 Java面试真题集锦目录…1大厂面试的基本流程 17字节跳动 17阿里 17腾讯 18网易游戏 18面试前需要准备: 18面试中可以借鉴的套路 19用简历争取到更多的面试机会 19自我介绍时,立即全面抛出技术栈和亮点 20先介绍项目背景,打消面试官疑问 21通过说项目管理工具,说明你不仅会写代码 22用SPRINGBOOT项目举例,说明你的技术基本面 23用实例说明你在内存调优方面的经验 24展示你在数据库调优方面的经验 25总结前文说辞 26准备项目说辞时,更可以准备后继面试官的问

    2022年7月8日
    53
  • 微服务调用链路追踪_区块链地址追踪

    微服务调用链路追踪_区块链地址追踪目录第一章Sleuth+Zipkin介绍1.1、什么是Sleuth1.2、什么是Zipkin第二章Sleuth+Zipkin入门案例2.1、项目准备与启动2.2、搭Zipkin服务端2.3、搭Zipkin客户端2.4、链路跟踪的测试配套资料,免费下载链接:https://pan.baidu.com/s/1la_3-HW-UvliDRJzfBcP_w提取码:lxfx复制这段内容后打开百度网盘手机App,操作更方便哦第一章Sleuth+Zipkin介绍1.1、什么是Sleuth我们已经接触

    2025年7月10日
    54
  • Maven环境变量配置

    Maven环境变量配置

    2021年10月1日
    55
  • 功能测试数据测试之因果图分析方法[通俗易懂]

    功能测试数据测试之因果图分析方法[通俗易懂]定义是一种利用图解法分析输入的各种组合情况,从而设计测试用例的方法,它适合于检查程序输入条件的各种组合情况。因果图法产生的背景等价类划分法和边界值分析方法都是着重考虑输入条件,但没有考虑输入条件的各种组合、输入条件之间的相互制约关系。这样虽然各种输入条件可能出错的情况已经测试到了,但多个输入条件组合起来可能出错的情况却被忽视了。如果在测试时必须考虑输入条件的各种组合,则可能的组合数目将是天文数字,因此必须考虑采用一种适合于描述多种条件的组合、相应产生多个动作的形式来进行测试用例的设计,这就需要利用

    2022年8月14日
    7
  • 小鹤双拼尝试

    小鹤双拼尝试26个英文字母除了a,o,e,i,u,v(代表ü,下同)之外都被用作声母,而除了这20个声母之外还有3个声母分别为双字母的zh,ch,sh,汉字中绝大部分字的读音都是声母加韵母组成的。而韵母中,除了上述提到的六个字母,其余韵母都是由两个或三个字母组成。小鹤双拼编码方案:第一个字母表示声母,第二字母表示韵母,没有声母时用零声母代替单字母声母韵母键位不变,就意味着26个字母都是和它本来的音相同,比较特殊的就是v,i,u三个(它们也可以拼成yu,yi,wu),当他们被第一个按下时,会代表声母zh,ch,sh

    2022年6月29日
    21
  • vue2使用animate css[通俗易懂]

    vue2使用animate css[通俗易懂]vue2使用animatecss先上几个链接vue插件大集合:awesome-vuevue2插件:vue2-animate:vue2-animatevue2插件vue2-animateDEMO:vue2-animatedemo:vue2-animate-demo我想用过animatecss的都知道这是一个极其简单而又酷炫的css动画库,但是我想在vue2中使用anima

    2022年7月12日
    23

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号