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 更多知识点

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

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • js整除取余(python)

    1.丢弃小数部分,保留整数部分parseInt(5/2)22.向上取整,有小数,则整数部分加1Math.ceil(5/2)33.四舍五入Math.round(5/2)34.向下取整Math.floor(5/2)2取余数0%401

  • 自动化运维架构(开发)

    自动化运维架构(开发)一、DevOps定义1.DevOps是“开发”和“运维”的缩写。2.DevOps是一组最佳实践强调(IT研发、运维、测试)在应用和服务生命周期中的协作和沟通3.强调整个组织的合作以及交付和基础设施变更自动化,从而实现持续集成、持续部署和持续交付二、DevOps持续交付环1.我们把开发交付划分为:计划–>编码–>构建–>测试–>…

  • HD251HJ_HB5082

    HD251HJ_HB5082原题链接描述Givenasequence1,2,3,……N,yourjobistocalculateallthepossiblesub-sequencesthatthesumofthesub-sequenceisM.输入Inputcontainsmultipletestcases.eachcasecontainstwoint…

  • 给织梦DEDECMS添加栏目图片与英文名显示

    给织梦DEDECMS添加栏目图片与英文名显示

  • C++之运算符重载

    C++预定义中的运算符的操作对象只限于基本的内置数据类型,但是对于我们自定义的类型(类)是没有办法操作的,但是大多时候我们需要对我们定义的类型进行类似的运算,这个时候就需要我们对运算符进行重新定义,赋

    2021年12月19日
  • navicat激活码(破解版激活)「建议收藏」

    navicat激活码(破解版激活),https://javaforall.cn/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

发表回复

您的电子邮箱地址不会被公开。

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