编辑
2020-12-12
客户端技术
00
请注意,本文编写于 1024 天前,最后修改于 113 天前,其中某些信息可能已经过时。

目录

TCP/UDP 简单示例
Client移植到Android
Android访问HTTPS
1、不校验证书(不推荐)
2、校验证书(推荐)

在Android中使用HTTPS的场景比较频繁,所以对于HTTPS的证书应该如何校验呢?关于HTTPS的校验原理可以参考我之前写的一篇文章:《 HTTPS协议实现原理 》,相信看完后应该对HTTPS有一个比较大致的了解。而且对HTTP(s)请求的工具进行了封装,需要体会这种封装工具类的思路,也就是编码中常见的Listener机制。然后是Android中TCP、UDP通信的例子,主要是把Android设备作为Client端,如果对Java的Socket编程比较熟悉的话,这些都是特别简单的示例程序,非常容易看懂。

TCP/UDP 简单示例

下面的例子演示了Client向Server发送了一串小写英文,Server返回大写字符串的功能:

UDPServer.java:

java
public class UDPServer { private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) throws Exception { DatagramSocket datagramSocket; datagramSocket = new DatagramSocket(8090); byte[] buf; DatagramPacket packet; while (true){ buf = new byte[1024]; packet = new DatagramPacket(buf, buf.length); datagramSocket.receive(packet); String content = new String(packet.getData()); InetAddress address = packet.getAddress(); System.out.println(format.format(new Date()) + "-" + address + "-" + content); int port = packet.getPort(); String replyContent = content.toUpperCase(); byte[] sendData = replyContent.getBytes(); DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, address, port); datagramSocket.send(sendPacket); } } }

UDPClient.java:

java
public class UDPClient { private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) throws Exception { System.out.println("请输入一句英文,服务器会返回其大写形式[exit退出]"); Scanner scanner = new Scanner(System.in); InetAddress address = InetAddress.getLocalHost(); DatagramPacket packet; DatagramSocket socket = new DatagramSocket(); while(true){ String line = scanner.nextLine(); if("exit".equals(line)) break; byte[] bytes = line.getBytes(); packet = new DatagramPacket(bytes, bytes.length, address, 8090); socket.send(packet); byte[] recvBuf = new byte[1024]; DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length); socket.receive(recvPacket); System.out.println(format.format(new Date()) + "-" + address + "-" + new String(recvBuf)); } socket.close(); } }

TCPServer.java:

java
public class TCPServer { static SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9090); while (true){ Socket socket = serverSocket.accept(); InetAddress address = socket.getInetAddress(); InputStream is = socket.getInputStream(); byte[] readBuf = new byte[1024]; try{ int len = is.read(readBuf); String recv = new String(readBuf, 0, len); System.out.println(format.format(new Date()) + "-" + address + "-" + recv); OutputStream os = socket.getOutputStream(); os.write(recv.toUpperCase().getBytes()); } catch (SocketException e){ System.err.println("客户端未发送信息"); } finally { socket.close(); } } } }

TCPClient.java:

java
public class TCPClient { private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) throws Exception { System.out.println("请输入一句英文,服务器会返回其大写形式[exit退出]"); Scanner scanner = new Scanner(System.in); while(true){ Socket socket = new Socket("127.0.0.1", 9090); String line = scanner.nextLine(); if("exit".equals(line)) break; OutputStream os = socket.getOutputStream(); os.write(line.getBytes()); InputStream is = socket.getInputStream(); byte[] readBuf = new byte[1024]; String recv = new String(readBuf, 0, is.read(readBuf)); InetAddress address = socket.getInetAddress(); System.out.println(format.format(new Date()) + "-" + address + "-" + recv); socket.close(); } } }

Client移植到Android

将两个Client移植到Android:

activity_main.xml

xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="10dp" tools:context=".MainActivity"> <EditText android:hint="输入发送内容" android:id="@+id/et_content" android:layout_width="match_parent" android:layout_height="wrap_content"/> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:text="192.168.1.113:8090" android:id="@+id/et_udp_server" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content"> </EditText> <Button android:text="UDP发送" android:onClick="sendUdpMessage" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content"/> </LinearLayout> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:text="192.168.1.113:9090" android:id="@+id/et_tcp_server" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content"> </EditText> <Button android:text="TCP发送" android:onClick="sendTcpMessage" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content"/> </LinearLayout> <TextView android:id="@+id/tv_show" android:text="收到回复:" android:layout_width="match_parent" android:layout_height="wrap_content"/> </LinearLayout>

MainActivity.java:

java
public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private static final SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss", Locale.CHINA); private EditText etInput; private TextView textView; private EditText udpServerET; private EditText tcpServerET; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etInput = findViewById(R.id.et_content); textView = findViewById(R.id.tv_show); udpServerET = findViewById(R.id.et_udp_server); tcpServerET = findViewById(R.id.et_tcp_server); } public void sendTcpMessage(View view) { String[] tcpInfo = tcpServerET.getText().toString().split(":"); String inputContent = etInput.getText().toString(); new Thread(()->{ try (Socket socket = new Socket(tcpInfo[0], Integer.parseInt(tcpInfo[1]))){ OutputStream os = socket.getOutputStream(); os.write(inputContent.getBytes()); InputStream is = socket.getInputStream(); byte[] readBuf = new byte[1024]; String recv = new String(readBuf, 0, is.read(readBuf)); InetAddress address = socket.getInetAddress(); String ret = String.format("%s-%s-%s", df.format(new Date()), address, recv); runOnUiThread(()-> textView.setText(ret)); }catch (IOException e){ Log.e(TAG, "sendTcpMessage: Error!"); } }).start(); } public void sendUdpMessage(View view) { String[] udpInfo = udpServerET.getText().toString().split(":"); String inputContent = etInput.getText().toString(); new Thread(()->{ try { DatagramSocket socket = new DatagramSocket(); byte[] bytes = inputContent.getBytes(); InetAddress address = InetAddress.getByName(udpInfo[0]); int serverPort = Integer.parseInt(udpInfo[1]); DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, serverPort); socket.send(packet); byte[] recvBuf = new byte[1024]; DatagramPacket recvPacket = new DatagramPacket(recvBuf, recvBuf.length); socket.receive(recvPacket); String ret = String.format("%s-%s-%s", df.format(new Date()), address, new String(recvBuf)); runOnUiThread(()-> textView.setText(ret)); }catch (IOException e){ Log.e(TAG, "sendUdpMessage: Error!"); } }).start(); } }

AndroidManifest.xml:

xml
<uses-permission android:name="android.permission.INTERNET"/>

注意点:1、网络访问权限 2、子线程代码中使用runOnUiThread()方法可更新UI

Android访问HTTPS

对于一个普通的HTTP请求,我们可以使用如下方式来发起请求,下面是一个简易的Http请求工具类:

java
public class HttpUtils { private static Handler mUIHandler = new Handler(Looper.getMainLooper()); interface HttpListener { void onSuccess(String content); void onFail(Exception e); } public static void doGet(String urlStr, HttpListener listener) { new Thread(() -> { Looper.prepare(); try { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.connect(); try (InputStream is = conn.getInputStream(); InputStreamReader reader = new InputStreamReader(is) ) { char[] buf = new char[4096]; int len; StringBuilder sb = new StringBuilder(); while ((len = reader.read(buf)) != -1) { sb.append(new String(buf, 0, len)); } mUIHandler.post(() -> listener.onSuccess(sb.toString())); } catch (IOException e) { e.printStackTrace(); listener.onFail(e); } }catch (IOException e){ e.printStackTrace(); listener.onFail(e); } }).start(); } }

1、不校验证书(不推荐)

MyX509TrustManager.java,MyX509TrustManager实现不做任何事情:

java
... import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; public class MyX509TrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // TODO... } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // TODO... } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }

HttpsUtils.java

java
... public class HttpsUtils { private static Handler mUIHandler = new Handler(Looper.getMainLooper()); interface HttpListener { void onSuccess(String content); void onFail(Exception e); } public static void doGet(Context context, String urlStr, HttpListener listener) { new Thread(() -> { Looper.prepare(); try { URL url = new URL(urlStr); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); SSLContext sslContext = SSLContext.getInstance("TLS"); // 放入自定义的MyX509TrustManager对象即可 TrustManager[] trustManagers = {new MyX509TrustManager()}; sslContext.init(null, trustManagers, new SecureRandom()); conn.setSSLSocketFactory(sslContext.getSocketFactory()); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.connect(); try (InputStream is = conn.getInputStream(); InputStreamReader reader = new InputStreamReader(is) ) { char[] buf = new char[4096]; int len; StringBuilder sb = new StringBuilder(); while ((len = reader.read(buf)) != -1) { sb.append(new String(buf, 0, len)); } mUIHandler.post(() -> listener.onSuccess(sb.toString())); } catch (IOException e) { e.printStackTrace(); listener.onFail(e); } }catch (Exception e){ e.printStackTrace(); listener.onFail(e); } }).start(); } }

2、校验证书(推荐)

拿我自己的博客站点来说,想要获得证书只需要在浏览器下载对应的证书即可(选择DER编码二进制和Base64编码均可),保存了一个名为srca.cer的文件到桌面:

将这份证书文件复制到项目的src/main/assets/目录下,没有assets就新建,所以完整路径为src/main/assets/srca.cer。

接下来需要实现MyX509TrustManager.java中的方法:

java
public class MyX509TrustManager implements X509TrustManager { private static final String TAG = "MyX509TrustManager"; // 证书对象 private X509Certificate serverCert; public MyX509TrustManager(X509Certificate serverCert) { this.serverCert = serverCert; } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 遍历证书 for (X509Certificate certificate: chain){ // 校验合法性与是否过期 certificate.checkValidity(); try { // 校验公钥 PublicKey publicKey = serverCert.getPublicKey(); certificate.verify(publicKey); } catch (Exception e) { throw new CertificateException(e); } } } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }

同时,将使用keyStore这个API来获取TrustManager数组,HttpsUtils.java如下:

java
public class Https2Utils { private static Handler mUIHandler = new Handler(Looper.getMainLooper()); interface HttpListener { void onSuccess(String content); void onFail(Exception e); } public static void doGet(Context context, String urlStr, HttpListener listener) { new Thread(() -> { Looper.prepare(); try { URL url = new URL(urlStr); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); SSLContext sslContext = SSLContext.getInstance("TLS"); X509Certificate serverCert = getCert(context); String defaultType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(defaultType); keyStore.load(null); // 别名、证书 keyStore.setCertificateEntry("srca", serverCert); String algorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); trustManagerFactory.init(keyStore); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); sslContext.init(null, trustManagers, new SecureRandom()); conn.setSSLSocketFactory(sslContext.getSocketFactory()); // 校验域名是否合法 conn.setHostnameVerifier((hostname, session) -> { HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier(); return verifier.verify("zouchanglin.cn", session); }); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.connect(); try (InputStream is = conn.getInputStream(); InputStreamReader reader = new InputStreamReader(is) ) { char[] buf = new char[4096]; int len; StringBuilder sb = new StringBuilder(); while ((len = reader.read(buf)) != -1) { sb.append(new String(buf, 0, len)); } mUIHandler.post(() -> listener.onSuccess(sb.toString())); } catch (IOException e) { e.printStackTrace(); listener.onFail(e); } }catch (Exception e){ e.printStackTrace(); listener.onFail(e); } }).start(); } private static X509Certificate getCert(Context context) { try { // src/main/assets/srca.cer InputStream inputStream = context.getAssets().open("srca.cer"); CertificateFactory factory = CertificateFactory.getInstance("X.509"); return (X509Certificate) factory.generateCertificate(inputStream); } catch (IOException | CertificateException e) { e.printStackTrace(); } return null; } }

在MainActivity中使用也很简单:

java
public class MainActivity extends AppCompatActivity { private EditText etUrl; private TextView tvShow; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etUrl = findViewById(R.id.et_url); tvShow = findViewById(R.id.tv_show); } public void loadContent(View view) { String url = etUrl.getText().toString(); Https2Utils.doGet(this, url, new Https2Utils.HttpListener() { @Override public void onSuccess(String content) { tvShow.setText(content); } @Override public void onFail(Exception e) { Toast.makeText(MainActivity.this, "Failed!", Toast.LENGTH_SHORT).show(); } }); } }

原文地址《Android Socket与HTTPS校验》

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!