Android中除了Binder之外,还有其他的跨进程通信。socket就是其中之一,多用于进程的启动还有日志等模块中的通信,是Android系统不可缺少的一部分。

socket简单实现(Java版)

1socket

专业术语来讲是套接字。socket的英文翻译为插座。把插座的含义引申开来,可以认为是一端接电源插头,另外插座上面的槽,可以提供多个其他设备使用。这里的电源插头端可以理解为是服务端,能够提供客户端交互的一端,插座上的槽可以理解为是客户端,能够尝试与服务端通信的一端。一旦通过connect连接(通电)起来就把的通信两端通过socket连接起来了。

TCPUDP

解释TCPUDP之前,先阐述一下OSI模型

  1. 物理层(Physical):单位是bit比特

    设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的 环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等。 在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理

  2. 数据链路层(Datalink):单位是

    可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行 数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等。 另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是 长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。 每次通信都要经过建立通信联络和拆除通信联络两过程。这种建立起来的数据收发关系~ 该层的设备有:网卡,网桥,网路交换机

  3. 网络层(Network):单位是数据包

    主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发 送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了 把任意两台终端设备数据链接起来的问题。简单点说就是:建立网络连接和为上层提供服务! 该层的设备有:路由。另外IP协议就在这一层。

  4. 传输层(Transport):单位是数据段

    向上面的应用层提供通信服务,面向通信部分的最高层,同时也是 用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络 层,并且保证这些数据段有效的到达对端。而这层有两个很重要 的协议就是:TCP传输控制协议与**UDP用户数据报协议**

  5. 会话层(Session)

    负责在网络中的两节点之间建立、维持和终止通信。建立通信链接, 保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时 决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!

  6. 表示层(Presentation)

    对来自应用层的命令和数据进行解释,对各种语法赋予相应 的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、 数据格式转换和加密解密,压缩解压缩"等

  7. 应用层(Application)

    OSI参考模型的最高层,为用户的应用程序提供网络服务。 它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。

2Android中socket通信的简单实现(TCP)

考虑到Android中的socket实现,会被网络请求的框架封装起来,比如OkhttpRetrofit等。所以下面所示都是直接使用socket API

这里先展示TCP传输协议的socket

2.1java中的socket接口

这要是需要认识两个类,一个是ServerSocket.java,另一个是Socket.java,这两个是对外提供的API接口。

2.2 ServerSocket构造

  1. 有参构造方法都会使服务器与特定端口绑定,该端口由参数port指定。端口被占用或是没有以超级用户身份运行服务器程序(比如绑定到1-1023的端口),会抛出BindException
  2. 有参构造里面还有一个backlog,用于设定客户连接请求队列的长度。服务端进程运行时,可同时监听到多个客户的连接请求。这里用于客户端请求连接服务端的队列,默认为50
 1//服务端
 2ServerSocket serverSocket = null;
 3public final int port = 2022;
 4try {
 5    InetAddress addr = InetAddress.getLocalHost();
 6    System.out.println("local host:"+addr);
 7    serverSocket = new ServerSocket(port);
 8    Log.d(TAG,"->>>serverSocket success");
 9} catch (IOException e) {
10    e.printStackTrace();
11}

2.3 accept

一种阻塞的方法,如果没有客户端对其服务端有连接,那么就一直等待。

ServerSocketaccept方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,并将它返回。如果队列中没有连接请求,accept方法就会一直等待,直到接收到了连接请求才返回。

 1//服务端
 2//考虑到不只有一个客户端连接,所以需要一个循环
 3try {
 4    Socket socket = null;
 5    //等待连接,每建立一个连接,就新建一个线程
 6    while(true){
 7        //等待一个客户端的连接,在连接之前,此方法是阻塞的
 8        socket = serverSocket.accept();
 9        Log.d(TAG, "->>>connect to"+socket.getInetAddress()+":"+socket.getLocalPort());
10    }
11} catch (IOException e) {
12    e.printStackTrace();
13}

2.4 Socket初始化

无参构造可设置超时时间。当客户端Socket构造方法与服务器建立连接时,需要等待一段时间,默认会一直等待下去,直到连接成功,或出现异常。

有参省略了无参的步骤,不需要设置远端地址和连接操作。

IP地址获取举例

返回本地主机的IP地址

1InetAddress addr1 = InetAddress.getLocalHost();

返回代表 “36.152.44.95"的 IP地址,这个实际上是百度的IP

1InetAddress addr2 = InetAddress.getByName("36.152.44.95");

返回域名为"yangyang48.github.io"的域名

1InetAddress addr3 = InetAddress.getByName("yangyang48.github.io");
2String name = address.getHostName();
3String ip = address.getHostAddress();
4//日志打印yangyang48.github.io---185.199.109.153
5Log.d(TAG, name + "---" + ip);

注意点addr3这个例子不能放在UI子线程中,可以放在子线程中。因为输入网址是一个耗时操作,主线程会因此异常。

 1//客户端
 2public static String IP_ADDRESS = "";
 3public static int PORT = 2022;
 4
 5IP_ADDRESS = tv_adress.getText().toString();
 6//这里的message为自己手动输入的字符串
 7new ConnectionThread(message).start();
 8
 9//新建一个子线程,实现socket通信
10class ConnectionThread extends Thread {
11    String message = null;
12
13    public ConnectionThread(String msg) {
14        Log.d(TAG, "->>>ConnectionThread|msg = " + msg);
15        message = msg;
16    }
17
18    @Override
19    public void run() {
20        if (soc == null) {
21            try {
22                Log.d(TAG,"->>>new socket");
23                if ("".equals(IP_ADDRESS)) {
24                    return;
25                }
26                //这个是阻塞的方法,连接成功后才会执行后面的语句
27                soc = new Socket(IP_ADDRESS, PORT);
28            } catch (IOException e) {
29                e.printStackTrace();
30            }
31        }
32    }
33}

除上述之外,还有一些获取Socket方法的信息

方法 含义
getInetAddress 获得连接的远程服务器的IP地址
etRemoteSocketAddress 获取连接的远程服务器地址
getLocalSocketAddress 获取本地绑定的地址
getPort 获得远程服务器的端口
getLocalAddress 获得客户本地的IP地址
getLocalPort 获得客户本地的端口
getInputStream 获得输入流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownInput方法关闭输入流,那么此方法会抛出IOException
getOutputStream 获得输出流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownOutput方法关闭输出流,那么此方法会抛出IOException

2.5获取输入输出流

因为存在服务端具有同时为多个客户提供服务的能力,所以每一次服务端都新连接都需要起一个线程来满足应对多个客户端。

 1//服务端
 2try {
 3    Socket socket = null;
 4    while(true){
 5        socket = serverSocket.accept();
 6        //每次连接都开启一个线程,最好使用JDK的线程池
 7        new ConnectThread(socket).start();
 8    }
 9} catch (IOException e) {
10    e.printStackTrace();
11}
12
13//向客户端发送信息
14class ConnectThread extends Thread{
15    Socket socket = null;
16    //socket变量注入
17    public ConnectThread(Socket socket){
18        super();
19        this.socket = socket;
20    }
21
22    @Override
23    public void run(){
24        try {
25            //这里通过socket来获取二进制的输入输出流用于传递数据
26            DataInputStream dis = new DataInputStream(socket.getInputStream());
27            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
28            while(true){
29                i++;
30                String msgRecv = dis.readUTF();
31                Log.d(TAG, "->>>msg from client: " + msgRecv);
32                //服务端的消息,只是在客户端的基本上加了一个符号
33                dos.writeUTF(msgRecv + i);
34                dos.flush();
35            }
36        } catch (IOException e) {
37            e.printStackTrace();
38        }
39    }
40}

跟上面的服务端的流程类似,客户端也有相似的流程

 1//客户端
 2public static String IP_ADDRESS = "";
 3public static int PORT = 2022;
 4String messageRecv = null;
 5
 6IP_ADDRESS = tv_adress.getText().toString();
 7//这里的message为自己手动输入的字符串
 8new ConnectionThread(message).start();
 9
10//新建一个子线程,实现socket通信
11class ConnectionThread extends Thread {
12    @Override
13    public void run() {
14        if (soc == null) {
15            try {
16                soc = new Socket(IP_ADDRESS, PORT);
17                //这里通过socket来获取二进制的输入输出流用于传递数据
18                dis = new DataInputStream(soc.getInputStream());
19                dos = new DataOutputStream(soc.getOutputStream());
20            } catch (IOException e) {
21                e.printStackTrace();
22            }
23        }
24        try {
25            //客户端发送消息
26            dos.writeUTF(message);
27            dos.flush();
28            //客户端接收消息
29            messageRecv = dis.readUTF();//如果没有收到数据,会阻塞
30            Message msg = new Message();
31            Bundle b = new Bundle();
32            b.putString("data", messageRecv);
33            msg.setData(b);
34            handler.sendMessage(msg);
35        } catch (IOException e) {
36            e.printStackTrace();
37        }
38    }
39}

上述socket通信传递的信息,主要是通过DataOutputStreamDataInputStream来传递,本质上还是用于数据的收发。

DataOutputStream数据输出流允许应用程序将基本Java数据类型写到基础输出流中,而DataInputStream数据输入流允许应用程序以机器无关的方式从底层输入流中读取基本的Java类型。

demo中的注意点

1.AndroidManifest.xml中需要添加<uses-permission android:name="android.permission.INTERNET" />

不添加上述权限,会影响下面检查权限时错误,在启动socket服务的时候会失败。

1java.net.SocketException: socket failed: EACCES (Permission denied)

2.服务端和客户端的端口号需要相同,且需要大于1023

即使IP相同。端口号不同也是会正常连接。包括正常的Socket初始化,但是去读写获取二进制流的时候,会读写错误。

端口号小于1024,可能需要设备的超级权限,默认设备user版本是无权限的。

3Android中socket通信的简单实现(UDP)

上面的TCP协议的Socket学习之后,继续来学习UDP协议的Socket

进行数据传输时,首先需要将要传输的数据定义成数据报(Datagram),在数据报中指明数据所要达到的Socket(主机地址和端口号),然后再将数据报发送出去。

除上述之外,还有一些获取DatagramPacket方法的信息

方法 含义
DatagramPacket() 构造数据报套接字并将其绑定到本地主机上任何可用的端口
DatagramPacket(int port) 创建数据报套接字并将其绑定到本地主机上的指定端口。
DatagramPacket(int port, InetAddress laddr) 创建数据报套接字,将其绑定到指定的本地地址。
getInetAddress 返回此套接字连接的地址
getPort 获得此套接字的端口
receive 从此套接字接收数据报包
send 从此套接字发送数据报包
close 关闭此数据报套接字

3.1创建服务端

  1. 创建服务器端DatagramSocket,指定端口号
  2. 创建数据报,用于接收客户端发送的数据
  3. 接收客户端发送的数据
  4. 读取数据
 1public void StartService() throws IOException {
 2    //1.创建服务器端DatagramSocket,指定端口号
 3    InetAddress addr = InetAddress.getLocalHost();
 4    datagramSocket = new DatagramSocket(PORT, addr);
 5    //2.创建数据报,用于接收客户端发送的数据
 6    byte[] data = new byte[1024];
 7    datagramPacket = new DatagramPacket(data, data.length);
 8    Log.d(TAG, "->>>UDP server is start ,wait client connected... ");
 9    //3.接收客户端发送的数据
10    //receive方法是阻塞方法
11    datagramSocket.receive(datagramPacket);
12    //4.读取数据
13    String info = new String(data, 0, datagramPacket.getLength());
14    Log.d(TAG, "->>>UDP server : info = " + info);
15
16    //响应客户端
17    //1.定于客户端地址、端口号、数据
18    InetAddress address = datagramPacket.getAddress();
19    int port = datagramPacket.getPort();
20    byte[] data2 = "hello".getBytes();
21    //2.创建数据报,包含响应的数据信息
22    DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port);
23    datagramSocket.send(packet2);
24    //4.关闭资源
25    //datagramSocket.close();
26}

3.2创建客户端

  1. 创建服务器端DatagramSocket,指定端口号
  2. 创建数据报,包含发送的数据信息
  3. 创建DatagramSocket对象
  4. 向服务器端发送数据报
 1public void StartClient() throws IOException {
 2    //1.创建服务器端DatagramSocket,指定端口号
 3    addr = InetAddress.getLocalHost();
 4    String IP_ADDRESS = addr.toString().split("/")[1];
 5    Log.d(TAG, "->>>addr = " + addr + " , IP_ADDRESS = " + IP_ADDRESS);
 6    //2.创建数据报,包含发送的数据信息
 7    byte[] data = "username:yangyang48;passwd:2023-01-01".getBytes();
 8    datagramPacket = new DatagramPacket(data, data.length, addr, PORT);
 9    //3.创建DatagramSocket对象
10    datagramSocket = new DatagramSocket();
11    //4.向服务器端发送数据报
12    datagramSocket.send(datagramPacket);
13
14    //接收服务端信息
15    //1.创建数据报,用于接收服务器端响应的数据
16    byte[] data2 = new byte[1024];
17    DatagramPacket packet2 = new DatagramPacket(data2, data2.length);
18    //2.接收服务器响应的数据
19    datagramSocket.receive(packet2);
20    //3.读取数据
21    String reply = new String(data2, 0, packet2.getLength());
22    Log.d(TAG, "->>>UDP client reply = " + reply);
23    //4.关闭资源
24    datagramSocket.close();
25}

特别注意

不管是客户端还是服务端,如果里面有一个方法是阻塞的,是不能够放入UI主线程的,需要放到子线程中。类似出现下面错误

1Caused by: android.os.NetworkOnMainThreadException
2            at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1145)
3            at java.net.InetAddress.lookupHostByName(InetAddress.java:385)
4            at java.net.InetAddress.getAllByNameImpl(InetAddress.java:236)
5            at java.net.InetAddress.getAllByName(InetAddress.java:214)

其中,getLocalHostreceive方法都是阻塞的

4总结

不管是TCP还是UDP,最根本的差异是传输层协议本身的差别,API的差异并不是很大,只要记住常见的API调用就能应付大多数的场景。在上层使用Socket相对比较少,更多的是native层使用进程bin,动态库so的方式调用底层socket方法建立网络连接。

异同 TCP UDP
IP地址(相同) 为了实现网络种不同的终端之间的通信,每个中断都必须有一个唯一的IP地址标识 为了实现网络种不同的终端之间的通信,每个中断都必须有一个唯一的IP地址标识
端口号(相同) 可使用的端口号是1024-65535 可使用的端口号是1024-65535
TCP协议与UDP协议(差异) TCP是可靠的链接,三次握手四次挥手 非连接的协议,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段

5源码下载

源码下载,点击这里

参考

[1] 盖了帽, 简单说一下DataInputStream和DataOutputStream这两个流, 2020.

[2] 沉沦者,android socket 学习及示例, 2021.

[3] c小旭, Android Socket通信简单实现, 2021.

[4] feng海涛, Android中socket通信的简单实现, 2020.

[5] Sally-he, Java中Socket的用法–Spring MVC, 2017.

[6] yjy239, Android socket源码解析(一)socket的初始化原理, 2021.

[7] 桥头放牛娃, ServerSocket详解, 2018.

[8] 菜鸟教程, 7.6.1 Socket学习网络基础准备, 2015.