socket简单实现(Java版)
900 Words|Read in about 5 Min|本文总阅读量次
Android中除了Binder之外,还有其他的跨进程通信。socket就是其中之一,多用于进程的启动还有日志等模块中的通信,是Android系统不可缺少的一部分。
socket简单实现(Java版)
1socket
专业术语来讲是套接字。socket
的英文翻译为插座。把插座的含义引申开来,可以认为是一端接电源插头,另外插座上面的槽,可以提供多个其他设备使用。这里的电源插头端可以理解为是服务端,能够提供客户端交互的一端,插座上的槽可以理解为是客户端,能够尝试与服务端通信的一端。一旦通过connect
连接(通电)起来就把的通信两端通过socket
连接起来了。
TCP
和UDP
解释
TCP
和UDP
之前,先阐述一下OSI
模型
物理层(
Physical
):单位是bit比特
设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的 环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等。 在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理
数据链路层(
Datalink
):单位是帧
可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行 数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等。 另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是 长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。 每次通信都要经过建立通信联络和拆除通信联络两过程。这种建立起来的数据收发关系~ 该层的设备有:网卡,网桥,网路交换机
网络层(
Network
):单位是数据包
主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发 送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了 把任意两台终端设备数据链接起来的问题。简单点说就是:建立网络连接和为上层提供服务! 该层的设备有:路由。另外
IP
协议就在这一层。传输层(
Transport
):单位是数据段
向上面的应用层提供通信服务,面向通信部分的最高层,同时也是 用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络 层,并且保证这些数据段有效的到达对端。而这层有两个很重要 的协议就是:
TCP
传输控制协议与**UDP
用户数据报协议**会话层(
Session
)负责在网络中的两节点之间建立、维持和终止通信。建立通信链接, 保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时 决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!
表示层(
Presentation
)对来自应用层的命令和数据进行解释,对各种语法赋予相应 的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、 数据格式转换和加密解密,压缩解压缩"等
应用层(
Application
)
OSI
参考模型的最高层,为用户的应用程序提供网络服务。 它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP
)、远程登录服务(Telnet
)、电子邮件服务(
2Android中socket通信的简单实现(TCP)
考虑到Android
中的socket
实现,会被网络请求的框架封装起来,比如Okhttp
,Retrofit
等。所以下面所示都是直接使用socket API
。
这里先展示TCP
传输协议的socket
。
2.1java中的socket接口
这要是需要认识两个类,一个是ServerSocket.java
,另一个是Socket.java
,这两个是对外提供的API
接口。
2.2 ServerSocket构造
- 有参构造方法都会使服务器与特定端口绑定,该端口由参数
port
指定。端口被占用或是没有以超级用户身份运行服务器程序(比如绑定到1-1023
的端口),会抛出BindException
。 - 有参构造里面还有一个
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
一种阻塞的方法,如果没有客户端对其服务端有连接,那么就一直等待。
ServerSocket
的accept
方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的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
通信传递的信息,主要是通过DataOutputStream
和DataInputStream
来传递,本质上还是用于数据的收发。
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创建服务端
- 创建服务器端
DatagramSocket
,指定端口号 - 创建数据报,用于接收客户端发送的数据
- 接收客户端发送的数据
- 读取数据
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创建客户端
- 创建服务器端
DatagramSocket
,指定端口号 - 创建数据报,包含发送的数据信息
- 创建
DatagramSocket
对象 - 向服务器端发送数据报
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)
其中,
getLocalHost
和receive
方法都是阻塞的
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.