本项目demo:DEMO
下载麻烦点下小星星,谢谢!
socket是一个常见的网络协议。这篇文章我们通4过个简单的聊天室的例子,来实践一下TCP和UDP在socket中的应用。以及使用Https协议的时候,我们客户端所需要的操作。
在一切开始之前,需要在mainfest.xml文件中申请网络使用权限
.
.
.
这个案例中,有两个主要的类
具体的细节在代码的备注中,代码主要分为客户端和服务器端,模拟进行通讯。
package com.example.chatroom.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;
public class UdpServer {
private InetAddress mAddress;//指定IP地址
private int mPort = 7777; //用于指定监听的端口号
private DatagramSocket mSocket;
private Scanner scanner;
public static void main(String[] args) {
System.out.println("server is started!");
new UdpServer().start();
}
public UdpServer()
{
try {
mAddress = InetAddress.getLocalHost();//获取本机InetAddress实例
mSocket = new DatagramSocket(mPort,mAddress);//构造DatagramSocket对象
scanner = new Scanner(System.in);//获取输入
scanner.useDelimiter("\n");
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
}
}
public void start() {
while (true) {
byte[] bytes = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(bytes, bytes.length);//载体
try {
mSocket.receive(receivePacket);//收取服务器发来的信息
InetAddress address = receivePacket.getAddress();//获取报文中的ip地址
int port = receivePacket.getPort();//获取端口号
byte[] data = receivePacket.getData();//获取报文中的数据
String clientMessage = new String(data, 0, receivePacket.getLength());//用bute数组构建String对象
System.out.println("address:" + address + "," + "port:" + port + "," + "clientMessage:" + clientMessage);
String returnedMessage = scanner.next();//获取控制台的输入
byte[] reruened = returnedMessage.getBytes();//转换为byte数组
DatagramPacket sentPacket = new DatagramPacket(reruened, reruened.length, receivePacket.getSocketAddress());//构建成数据包包
mSocket.send(sentPacket);//发送数据报
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package com.example.chatroom.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;
public class UdpClient {
private String mServerIp = "192.168.3.8";//指定要访问服务的IP
private InetAddress mInetAddress;
private int mServerPort = 7777;//要访问的端口号
private DatagramSocket mSocket;
private Scanner mScanner;
public static void main(String[] args) {
System.out.println("client is started!");
new UdpClient().start();
}
public UdpClient()
{
try {
mSocket = new DatagramSocket();
mInetAddress = InetAddress.getByName(mServerIp);//构造InetAddress 对象
mScanner = new Scanner(System.in);
mScanner.useDelimiter("\n");
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
public void start()
{
while (true){
try {
String clientMessage = mScanner.next();//获取控制台输入的数据
byte[] clientMessageBytes = clientMessage.getBytes();
DatagramPacket clientPacket = new DatagramPacket(clientMessageBytes,
clientMessageBytes.length,mInetAddress,mServerPort);//将数据打包到DatagramPacket 中
mSocket.send(clientPacket);//通过socket发送数据
byte[] bytes = new byte[1024];
DatagramPacket serverMsgPacket = new DatagramPacket(bytes,bytes.length);
mSocket.receive(serverMsgPacket);//接收服务器发来的数据
// InetAddress address = serverMsgPacket.getAddress();
// int port = serverMsgPacket.getPort();
byte[] data = serverMsgPacket.getData();
String serverMessage = new String(data, 0, serverMsgPacket.getLength());
System.out.println( "serverMessage:" + serverMessage);//将数据打印在控制台
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
测试结果如下:
值得注意的是, mSocket.receive()方法在没有收到数据的时候会一直处于阻塞状态。
我们可以以将客户端的代码迁移到安卓上,布局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context=".UDPActivity">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="50dp">
<EditText
android:id="@+id/input"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content"
android:text="Hello World!" />
<Button
android:id="@+id/send"
android:text="发送"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
</LinearLayout>
<ScrollView
android:layout_marginTop="50dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
</RelativeLayout>
就是这个样子的:
迁移之后,安卓端代码如下:
首先我们将消息发送的业务逻辑迁移到一个新的类中,代码和之前客户端的基本一致:
package com.example.chatroom.biz;
import android.os.Handler;
import android.os.Looper;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class UdpClientBiz {
private String mServerIp = "192.168.2.101";
private InetAddress mInetAddress;
private int mServerPort = 7777;
private DatagramSocket mSocket;
private Handler mUIHandler = new Handler(Looper.getMainLooper());//将handler声明在主线程中,方便我们操作UI
//创建一个接口在进行收到消息和发生异常时候的回调
public interface OnMessageReturnListener
{
void onMessageReturn(String message);
void onError(Exception e);
}
public void sendMsg(final String msg, final OnMessageReturnListener onMessageReturnListener)
{
new Thread(){
@Override
public void run() {
try {
//sendmessage
byte[] clientMessageBytes = msg.getBytes();
DatagramPacket clientPacket = new DatagramPacket(clientMessageBytes,
clientMessageBytes.length,mInetAddress,mServerPort);
mSocket.send(clientPacket);
//reciveMessage
byte[] bytes = new byte[1024];
DatagramPacket serverMsgPacket = new DatagramPacket(bytes,bytes.length);
mSocket.receive(serverMsgPacket);
final String serverMessage = new String(serverMsgPacket.getData(), 0, serverMsgPacket.getLength());
mUIHandler.post(new Runnable() {
@Override
public void run() {
onMessageReturnListener.onMessageReturn(serverMessage);
}
});
System.out.println( "clientMessage:" + serverMessage);
} catch (final IOException e) {
e.printStackTrace();
mUIHandler.post(new Runnable() {
@Override
public void run() {
onMessageReturnListener.onError(e);
}
});
}
}
}.start();
}
public UdpClientBiz()
{
try {
mSocket = new DatagramSocket();
mInetAddress = InetAddress.getByName(mServerIp);
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
public void destory()
{
if (mSocket != null)
{
mSocket.close();
}
}
}
package com.example.chatroom;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.example.chatroom.biz.UdpClientBiz;
public class UDPActivity extends AppCompatActivity {
private EditText mEditText;
private Button mButtonSend;
private TextView mTextViewMessage;
private UdpClientBiz udpClientBiz = new UdpClientBiz();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("UDP_CHATROOM");
initView();
mButtonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String messages = mEditText.getText().toString();
if (TextUtils.isEmpty(messages))
{
return;
}
appendMessage2Content("client:"+messages);
udpClientBiz.sendMsg(messages, new UdpClientBiz.OnMessageReturnListener() {
@Override
public void onMessageReturn(String message) {
appendMessage2Content("server:"+message);
}
@Override
public void onError(Exception e) {
e.printStackTrace();
}
});
mEditText.setText(" ");
}
});
}
private void appendMessage2Content(String message) {
mTextViewMessage.append(message+"\n");
}
private void initView() {
mEditText = findViewById(R.id.input);
mButtonSend = findViewById(R.id.send);
mTextViewMessage = findViewById(R.id.message);
}
}
相比UDP而言,TCP就显得比较简洁。我们只需要通过ServerSocket的输入流/输出流来获取客户端/服务器发来的数据。
虽然比较简洁,但我们的这个例子却不太简单。这个案例实现的是多个客户端和服务器组成的多人聊天室。由于聊天室不是1对1的,所以需要一个消息缓存的机制,然后让服务器循环从消息池中取出消息,转发到客户端。
我们先看消息池是怎么样的:
package com.example.chatroom.tcp.server;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
public class MessagePool {
private static MessagePool messagePoolInstance = new MessagePool();//单例
private LinkedBlockingQueue<String> messageQueue = new LinkedBlockingQueue();//消息队列
private List<MessageConmingListener> msgListeners = new ArrayList<>();
public MessagePool() {
}
//获取单例,为例简单先不做锁
public static MessagePool getMessagePoolInstance()
{
return messagePoolInstance;
}
//发送消息的方法
public void sendMessage(String message)
{
try {
messageQueue.put(message);//将消息放入消息队列
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void start()
{
new Thread(){
@Override
public void run() {
while (true){
try {
String msg = messageQueue.take();//从消息队列中取出消息
notifiMessageComing(msg);//使用观察者模式,通知客户端新消息
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
//通知客户端新的消息
private void notifiMessageComing(String msg) {
for (MessageConmingListener listener:msgListeners) {
listener.onMessageComing(msg);
}
}
//消息监听器接口
public interface MessageConmingListener
{
void onMessageComing(String message);
}
//注册消息监听器
public void addMessageConmingListener(MessageConmingListener messageConmingListener)
{
msgListeners.add(messageConmingListener);
}
}
package com.example.chatroom.tcp.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public void start() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(9090);//监听9090端口
MessagePool.getMessagePoolInstance().start();//开始从消息池中取出消息,并发出去
while (true) {
Socket socket = serverSocket.accept();//接收客户端的请求,并拿到存储了客户端数据的Socket对象
System.out.println("ip:" + socket.getInetAddress().getHostAddress()
+ ", port:" + socket.getPort() + "is online now ...");
ClientTask clientTask = new ClientTask(socket);//给这个socket所代表的客户端注册一个ClientTask
MessagePool.getMessagePoolInstance().addMessageConmingListener(clientTask);//将ClientTask 加入到观察者中
clientTask.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TCPServer().start();
}
}
package com.example.chatroom.tcp.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
//实现了消息监听的接口,继承自Thread类
public class ClientTask extends Thread implements MessagePool.MessageConmingListener {
private Socket mSocket;
private InputStream inputStream;
private OutputStream outputStream;
public ClientTask(Socket Socket) {
try {
mSocket = Socket;
inputStream = mSocket.getInputStream();
outputStream = mSocket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));//用BufferReader从输入流中读取数据
try {
String line ;
while ((line = br.readLine())!=null)
{
System.out.println("read:"+line);
//转发消息给其他客户端
MessagePool.getMessagePoolInstance().sendMessage("port:"+mSocket.getPort()+" message:"+line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
//在消息池中被回调
@Override
public void onMessageComing(String message) {
try {
//向连接到服务的客户端写数据
outputStream.write(message.getBytes());
outputStream.write("\n".getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.example.chatroom.tcp.client;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
public class TCPClient {
private Scanner mScanner;
public TCPClient() {
this.mScanner = new Scanner(System.in);
mScanner.useDelimiter("\n");
}
public void start()
{
try {
Socket socket = new Socket("192.168.2.100",9090);//访问对应ip下的端口
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
final BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
new Thread(){
@Override
public void run() {
String line = null;
try {
while ((line = br.readLine()) != null) {
System.out.println(line);//将受到的消息打印到控制台
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}.start();
while (true)
{
//将控制台输入的消息发送到服务器
String msg = mScanner.next();
bw.write(msg);
bw.newLine();
bw.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new TCPClient().start();
}
}
效果如下:
我们可以在对应的socket做一些标识,例如昵称什么的,然后就可以得到和真正的聊天类似的效果。
然后我们将这个功能迁移带客户端:
我们新建一个业务类,将客户端中带部分代码分割出去:
package com.example.chatroom.biz;
import android.os.Handler;
import android.os.Looper;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class TCPClientBiz {
private Socket socket ;
private InputStream inputStream ;
private OutputStream outputStream ;
private Handler mUIHandler = new Handler(Looper.getMainLooper());
private OnMsgReceiveListener mListener;
public void setOnMsgReceiveListener(OnMsgReceiveListener onMsgReceiveListener) {
mListener = onMsgReceiveListener;
}
public TCPClientBiz() {
new Thread() {
@Override
public void run() {
try {
socket= new Socket("192.168.2.101",9090);
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
readServerMsg();
}
catch (final Exception e)
{
mUIHandler.post(new Runnable() {
@Override
public void run() {
if (mListener !=null) {
mListener.onError(e);
}
}
});
e.printStackTrace();
}
}
}.start();
}
public void sendMessage(final String msg ) {
new Thread() {
@Override
public void run() {
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
bw.write(msg);
bw.newLine();
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
private void readServerMsg() throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = br.readLine()) != null) {
final String finalLine = line;
mUIHandler.post(new Runnable() {
@Override
public void run() {
if (mListener !=null) {
mListener.onMessageReceived(finalLine);
}
}
});
}
}
public interface OnMsgReceiveListener
{
void onMessageReceived(String msg);
void onError(Exception e);
}
public void onDestory()
{
try {
if (socket!=null)
{
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (inputStream !=null)
{
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outputStream != null)
{
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.example.client;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.chatroom.biz.TCPClientBiz;
public class TCPActivity extends AppCompatActivity {
private EditText mEditText;
private Button mButtonSend;
private TextView mTextViewMessage;
private TCPClientBiz tcpClientbiz;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_t_c_p);
tcpClientbiz = new TCPClientBiz();
tcpClientbiz.setOnMsgReceiveListener(new TCPClientBiz.OnMsgReceiveListener() {
@Override
public void onMessageReceived(String msg) {
appendMessage2Content(msg);//将受到的消息放在textview中
}
@Override
public void onError(Exception e) {
}
});
initView();
mButtonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTextViewMessage.setText(" ");
String messages = mEditText.getText().toString();
if (TextUtils.isEmpty(messages))
{
return;
}
appendMessage2Content("client:"+messages);
tcpClientbiz.sendMessage(messages);
}
});
}
private void appendMessage2Content(String message) {
mTextViewMessage.append(message+"\n");
}
private void initView() {
mEditText = findViewById(R.id.input);
mButtonSend = findViewById(R.id.send);
mTextViewMessage = findViewById(R.id.message);
}
@Override
protected void onDestroy() {
super.onDestroy();
tcpClientbiz.onDestory();
}
}
这里需要注意的是,要将客户端的代码放在另外一个module 中,不然会运行不起来。
Https是是一种采用了加密算法的通讯协议。为了安全起见,在安卓端访问使用了Https协议的服务的时候(服务并未获得CA证书)需要校验证书以及域名,来保证数据的安全性。
案例三:证书校验以及域名校验
我们先写一个Utils来访问一个链接:
package https;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
public class HttpUtils {
private static Handler mUIHandler = new Handler(Looper.getMainLooper());
public interface HttpListener
{
void onSucceed(String content);
void onFailed(Exception e);
}
public static void doGet(final Context context,final String urlStr, final HttpListener httpListener)
{
new Thread()
{
@Override
public void run() {
InputStream inputStream = null;
try {
URL url = new URL(urlStr);
HttpsURLConnection httpConn = (HttpsURLConnection) url.openConnection();
//校验证书_方法1:
X509Certificate certificate = getCert(context);
TrustManager[] tm = {new MyX509TrustManager(certificate)};
SSLContext sslContext = SSLContext.getInstance("TLS");
//校验证书_方法2:
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null);
keyStore.setCertificateEntry("srca",certificate);
TrustManagerFactory trustManagerFactory
= TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] tm1 = trustManagerFactory.getTrustManagers();
//在获取了TrustManager[]数组之后的步骤就相同了
sslContext.init(null,tm,new SecureRandom());
httpConn.setSSLSocketFactory(sslContext.getSocketFactory());
//校验域名
httpConn.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
return defaultHostnameVerifier.verify("12306.cn",session);
}
});
httpConn.setDoInput(true);
httpConn.setDoOutput(true);
httpConn.setRequestMethod("GET");
httpConn.setConnectTimeout(5*1000);
httpConn.setReadTimeout(5*1000);
httpConn.connect();
final StringBuilder content = new StringBuilder();
inputStream = httpConn.getInputStream();
byte[] buf = new byte[2048];
int len = -1;
while ((len = inputStream.read(buf))!=-1)
{
content.append(new String(buf,0,len));
}
mUIHandler.post(new Runnable() {
@Override
public void run() {
httpListener.onSucceed(content.toString());
}
});
} catch (MalformedURLException e) {
e.printStackTrace();
httpListener.onFailed(e);
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} finally {
if (inputStream!=null)
{
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}.start();
}
private static X509Certificate getCert(Context context) {
try {
InputStream inputStream = context.getAssets().open("xxx.cer");//在本地assets目录下保存的网站证书
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
return (X509Certificate) certificateFactory.generateCertificate(inputStream);
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
}
return null;
}
}
其中,MyX509TrustManager类为:
package https;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class MyX509TrustManager implements X509TrustManager {
private X509Certificate mySeverCert;
public MyX509TrustManager(X509Certificate mySeverCert) {
this.mySeverCert = mySeverCert;
}
@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 {
certificate.verify(mySeverCert.getPublicKey());
} catch (Exception e) {
//出现错误代表证书验证失败
throw new CertificateException(e);
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
证书校验的两种方法,第一种需要我们自己去校验,第二种是调用API为我们校验。