android基于socket的局域网内服务器与客户端加密通信
作者:伍子夜
本篇文章主要介绍了android基于socket的局域网内服务器与客户端加密通信,这里整理了详细的代码,有需要的小伙伴可以参考下。
实现了基本的socket通信(即两台设备,一台用作服务器,一台用作客户端),服务器进行监听,客户端发送加密数据到服务器,服务器进行解密得到明文。
注意:本项目中使用了ButterKnife及EventBus作为辅助工具,通信建立时默认网络正常(未做局域网网络环境检测),加密方式为AES加密
1.效果图:
(1)客户端
(2)服务器端
2.界面布局部分
(1)服务器端布局 function_socket_server.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout style="@style/ToolBar"> <TextView style="@style/ToolBar_tv_Title" android:text="网络加密-服务器端" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btn_startListener" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="启动监听" /> <Button android:id="@+id/btn_stopListener" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="停止监听" /> <Button android:id="@+id/btn_getUser" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="刷新用户" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="10dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="本机地址:" /> <TextView android:id="@+id/tv_localAddress" android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="true" /> </LinearLayout> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="接收到的明文:" android:textColor="@color/black" /> <TextView android:id="@+id/tv_receivedContent" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="解密后的明文:" android:textColor="@color/black" /> <TextView android:id="@+id/tv_decryptContent" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp" /> </LinearLayout> </ScrollView> </LinearLayout>
(2)客户端布局 function_socket_client.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout style="@style/ToolBar"> <TextView style="@style/ToolBar_tv_Title" android:text="网络加密-客户端" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="10dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="服务器地址:" /> <EditText android:id="@+id/edtTxt_serverAddress" android:layout_width="match_parent" android:text="192.168.43.1" android:layout_height="wrap_content" android:singleLine="true" /> </LinearLayout> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="文本内容:" android:textColor="@color/black" /> <EditText android:id="@+id/edtTxt_Content" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/main_background" android:padding="10dp" android:text="123木头人" /> </LinearLayout> </ScrollView> <Button android:id="@+id/btn_encryptAndSend" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:text="加密并发送" /> </LinearLayout>
(3)用到的style
<!--通用Title的右侧按钮--> <style name="ToolBar_iv_Right"> <item name="android:layout_width">@dimen/toolbar_icon_dimen</item> <item name="android:layout_height">@dimen/toolbar_icon_dimen</item> <item name="android:layout_alignParentRight">true</item> <item name="android:layout_gravity">end</item> <item name="android:clickable">true</item> <item name="android:background">?android:actionBarItemBackground</item> <item name="android:padding">15dp</item> </style> <!--通用Title的TextView--> <style name="ToolBar_tv_Title"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">wrap_content</item> <item name="android:layout_centerVertical">true</item> <item name="android:layout_marginLeft">@dimen/toolbar_title_haveBack_marginStart</item> <item name="android:layout_marginRight">@dimen/toolbar_title_haveBack_marginEnd</item> <item name="android:gravity">center</item> <item name="android:singleLine">true</item> <item name="android:textColor">@color/white</item> <item name="android:textSize">20sp</item> </style>
3.功能代码
(1)基类 BaseEventActivity.Java
import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import org.greenrobot.eventbus.EventBus; import butterknife.ButterKnife; public abstract class BaseEventActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getIntentData(); setContentView(getLayoutResId()); ButterKnife.bind(this); EventBus.getDefault().register(this); init(); } protected void getIntentData() { } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } protected abstract void init(); protected abstract int getLayoutResId(); }
(2)服务器主界面 Function_Socket.java
import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.view.View; import android.widget.TextView; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.io.BufferedReader; import java.io.FileReader; import java.util.ArrayList; import butterknife.BindView; import butterknife.OnClick; /** * 服务器界面 */ public class Function_Socket_Server extends BaseEventActivity { @BindView(R.id.tv_localAddress) TextView tv_localAddress; @BindView(R.id.tv_receivedContent) TextView tv_receivedContent; @BindView(R.id.tv_decryptContent) TextView tv_decryptContent; private LocalService localService;//用于启动监听的服务 private ServiceConnection sc;//服务连接 @Override protected void init() { tv_localAddress.setText(ToolUtil.getHostIP()); sc = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { LocalService.LocalBinder localBinder = (LocalService.LocalBinder) service; localService = localBinder.getService(); localService.startWaitDataThread(); ToastUtil.showToast(Function_Socket_Server.this, "监听已启动"); } @Override public void onServiceDisconnected(ComponentName name) { } }; connection(); } @Subscribe(threadMode = ThreadMode.MAIN) public void getData(String data) { tv_receivedContent.setText(data); tv_decryptContent.setText(AESUtil.decrypt(ConstantUtil.password, data)); } /** * 绑定service */ private void connection() { Intent intent = new Intent(this, LocalService.class); bindService(intent, sc, BIND_AUTO_CREATE); } @Override protected int getLayoutResId() { return R.layout.function_socket_server; } /** * 获取连接到本机热点上的手机ip */ private ArrayList<String> getConnectedIP() { ArrayList<String> connectedIP = new ArrayList<>(); try { //通过读取配置文件实现 BufferedReader br = new BufferedReader(new FileReader( "/proc/net/arp")); String line; while ((line = br.readLine()) != null) { String[] splitted = line.split(" +"); if (splitted.length >= 4) { String ip = splitted[0]; connectedIP.add(ip); } } } catch (Exception e) { e.printStackTrace(); } return connectedIP; } @OnClick({R.id.btn_startListener, R.id.btn_stopListener, R.id.btn_getUser}) public void onClick(View v) { switch (v.getId()) { case R.id.btn_startListener://启动监听 connection(); break; case R.id.btn_stopListener://停止监听 if (sc != null) unbindService(sc); break; case R.id.btn_getUser://刷新连接到此设备的IP并清空之前接收到的数据 ArrayList<String> connectedIP = getConnectedIP(); StringBuilder resultList = new StringBuilder(); for (String ip : connectedIP) { resultList.append(ip); resultList.append("\n"); } ToastUtil.showToast(this, "连接到手机上的Ip是:" + resultList.toString()); tv_decryptContent.setText(""); tv_receivedContent.setText(""); break; } } public void onDestroy() { super.onDestroy(); if (sc != null) unbindService(sc); } }
(3)客户端主界面 Function_Socket_Client.java
import android.app.ProgressDialog; import android.util.Log; import android.view.View; import android.widget.EditText; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import butterknife.BindView; import butterknife.OnClick; /** * 客户端界面 */ public class Function_Socket_Client extends BaseEventActivity { @BindView(R.id.edtTxt_Content) EditText edtTxt_Content; @BindView(R.id.edtTxt_serverAddress) EditText edtTxt_serverAddress; private ProgressDialog mProgressDialog;//加载的小菊花 /** * 初始化 */ @Override protected void init() { } @Override protected int getLayoutResId() { return R.layout.function_socket_client; } @OnClick(R.id.btn_encryptAndSend) public void onClick(View v) { switch (v.getId()) { case R.id.btn_encryptAndSend: String s = edtTxt_Content.getText().toString().trim(); String ip = edtTxt_serverAddress.getText().toString().trim(); if (ToolUtil.IsIpv4(ip)) { new SendDataThread(ip, AESUtil.encrypt(ConstantUtil.password, s), ConstantUtil.port).start();//消息发送方启动线程发送消息 showProgressDialog("尝试发送数据到\n\t\t" + ip, true); } else { ToastUtil.showToast(this, "IP不合法!"); } break; } } /** * 连接结果 * * @param resultCode 0:连接超时;1:发送成功 2:失败 */ @Subscribe(threadMode = ThreadMode.MAIN) public void sendResult(Integer resultCode) { Log.i("succ", "=" + resultCode); dismissProgressDialog(); switch (resultCode) { case ConstantUtil.CODE_SUCCESS: ToastUtil.showToast(this, "发送成功"); break; case ConstantUtil.CODE_TIMEOUT: ToastUtil.showToast(this, "连接超时"); break; case ConstantUtil.CODE_UNKNOWN_HOST: ToastUtil.showToast(this, "错误-未知的host"); break; } } /** * 数据加载小菊花 * * @param msg 内容 * @param isCancel 是否允许关闭 true - 允许 false - 不允许 */ public void showProgressDialog(final String msg, final boolean isCancel) { runOnUiThread(new Runnable() { @Override public void run() { try { if (mProgressDialog == null) { mProgressDialog = new ProgressDialog(Function_Socket_Client.this); } if (mProgressDialog.isShowing()) { return; } mProgressDialog.setMessage(msg); mProgressDialog.setCancelable(isCancel); mProgressDialog.setCanceledOnTouchOutside(false); mProgressDialog.setOnCancelListener(null); mProgressDialog.show(); } catch (Exception e) { e.printStackTrace(); } } }); } /** * 隐藏数据加载的进度小菊花 **/ public void dismissProgressDialog() { try { if (mProgressDialog != null && mProgressDialog.isShowing()) { runOnUiThread( new Runnable() { @Override public void run() { mProgressDialog.dismiss(); } } ); } } catch (Exception e) { e.printStackTrace(); } } }
(4)LocalService.java
import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; /** * 此服务用于启动监听线程 */ public class LocalService extends Service { private IBinder iBinder = new LocalService.LocalBinder(); @Override public IBinder onBind(Intent intent) { return iBinder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } public void startWaitDataThread() { new ListenThread(ConstantUtil.port).start(); } //定义内容类继承Binder public class LocalBinder extends Binder { //返回本地服务 public LocalService getService() { return LocalService.this; } } }
(5)ListenThread.java
import org.greenrobot.eventbus.EventBus; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * 监听线程 */ public class ListenThread extends Thread { private ServerSocket serverSocket; public ListenThread(int port) { try { serverSocket = new ServerSocket(port); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { while (true) { try { if (serverSocket != null) { Socket socket = serverSocket.accept(); InputStream inputStream = socket.getInputStream(); if (inputStream != null) { BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); String str; str = in.readLine(); EventBus.getDefault().post(str); socket.close(); } } } catch (IOException e) { e.printStackTrace(); } } } }
(6)SendDataThread.java
import android.util.Log; import org.greenrobot.eventbus.EventBus; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketTimeoutException; import java.net.UnknownHostException; /** * 数据发送线程 */ public class SendDataThread extends Thread { private Socket socket; private String ip;//接收方的IP private int port;//接收方的端口号 private String data;//准备发送的数据 public SendDataThread(String ip, String data, int port) { this.ip = ip; this.data = data; this.port = port; } @Override public void run() { try { socket = new Socket(); socket.connect(new InetSocketAddress(ip,port),ConstantUtil.TIME_MILLIS);//设置超时时间 } catch (UnknownHostException e) { EventBus.getDefault().post(ConstantUtil.CODE_UNKNOWN_HOST); Log.d("error", "SendDataThread.init() has UnknownHostException" + e.getMessage()); } catch (SocketTimeoutException e) { EventBus.getDefault().post(ConstantUtil.CODE_TIMEOUT); Log.d("error", "SendDataThread.init() has TimeoutException:" + e.getMessage()); }catch (IOException e){ Log.d("error", "SendDataThread.init() has IOException:" + e.getMessage()); } if (socket != null&&socket.isConnected()) { try { OutputStream ops = socket.getOutputStream(); OutputStreamWriter opsw = new OutputStreamWriter(ops); BufferedWriter writer = new BufferedWriter(opsw); writer.write(data + "\r\n\r\n");//由于socket使用缓冲区进行读写数据,因此使用\r\n\r\n用于表明数据已写完.不加这个会导致数据无法发送 EventBus.getDefault().post(ConstantUtil.CODE_SUCCESS); writer.flush(); } catch (IOException e) { e.printStackTrace(); } } } }
(7)AESUtil.java
import android.util.Log; import java.io.UnsupportedEncodingException; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; /** * AES加密工具类 */ public class AESUtil { // private static final String CipherMode = "AES/ECB/PKCS5Padding";使用ECB加密,不需要设置IV,但是不安全 private static final String CipherMode = "AES/CFB/NoPadding";//使用CFB加密,需要设置IV /** * 生成加密后的密钥 * * @param password 密钥种子 * @return isSucceed */ private static SecretKeySpec createKey(String password) { byte[] data = null; if (password == null) { password = ""; } StringBuilder sb = new StringBuilder(32); sb.append(password); while (sb.length() < 32) { sb.append("0"); } if (sb.length() > 32) { sb.setLength(32); } try { data = sb.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return new SecretKeySpec(data, "AES"); } // /** 加密字节数据 **/ private static byte[] encrypt(byte[] content, String password) { try { SecretKeySpec key = createKey(password); System.out.println(key); Cipher cipher = Cipher.getInstance(CipherMode); cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec( new byte[cipher.getBlockSize()])); return cipher.doFinal(content); } catch (Exception e) { e.printStackTrace(); } return null; } // /** 加密(结果为16进制字符串) **/ public static String encrypt(String password, String content) { Log.d("加密前", "seed=" + password + "\ncontent=" + content); byte[] data = null; try { data = content.getBytes("UTF-8"); } catch (Exception e) { e.printStackTrace(); } data = encrypt(data, password); String result = byte2hex(data); Log.d("加密后", "result=" + result); return result; } // /** 解密字节数组 **/ private static byte[] decrypt(byte[] content, String password) { try { SecretKeySpec key = createKey(password); Cipher cipher = Cipher.getInstance(CipherMode); cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec( new byte[cipher.getBlockSize()])); return cipher.doFinal(content); } catch (Exception e) { e.printStackTrace(); } return null; } // /** 解密16进制的字符串为字符串 **/ public static String decrypt(String password, String content) { Log.d("解密前", "seed=" + password + "\ncontent=" + content); byte[] data = null; try { data = hex2byte(content); } catch (Exception e) { e.printStackTrace(); } data = decrypt(data, password); if (data == null) return null; String result = null; try { result = new String(data, "UTF-8"); Log.d("解密后", "result=" + result); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return result; } // /** 字节数组转成16进制字符串 **/ private static String byte2hex(byte[] b) { // 一个字节的数, StringBuilder sb = new StringBuilder(b.length * 2); String tmp ; for (byte aB : b) { // 整数转成十六进制表示 tmp = (Integer.toHexString(aB & 0XFF)); if (tmp.length() == 1) { sb.append("0"); } sb.append(tmp); } return sb.toString().toUpperCase(); // 转成大写 } // /** 将hex字符串转换成字节数组 **/ private static byte[] hex2byte(String inputString) { if (inputString == null || inputString.length() < 2) { return new byte[0]; } inputString = inputString.toLowerCase(); int l = inputString.length() / 2; byte[] result = new byte[l]; for (int i = 0; i < l; ++i) { String tmp = inputString.substring(2 * i, 2 * i + 2); result[i] = (byte) (Integer.parseInt(tmp, 16) & 0xFF); } return result; } }
(8)ConstantUti.java
/** * 常量类 */ public class ConstantUtil { public static final int TIME_MILLIS = 5 * 1000;//连接超时时间 public static final int port = 25256;//端口号 public static final String password = "123456885";//加密所使用的密钥 public static final int CODE_TIMEOUT = 0;//连接超时 public static final int CODE_SUCCESS = 1;//连接成功 public static final int CODE_UNKNOWN_HOST = 2;//错误-未知的host }
(9)ToolUtil.java
import android.util.Log; import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; /** * 工具类 */ public class ToolUtil { /** * 获取ip地址 * 如果是移动网络,会显示自己的公网IP,如果是局域网,会显示局域网IP * 因此本例中服务器端需要断开移动网络以得到本机局域网IP */ public static String getHostIP() { String hostIp = null; try { Enumeration nis = NetworkInterface.getNetworkInterfaces(); InetAddress ia; while (nis.hasMoreElements()) { NetworkInterface ni = (NetworkInterface) nis.nextElement(); Enumeration<InetAddress> ias = ni.getInetAddresses(); while (ias.hasMoreElements()) { ia = ias.nextElement(); if (ia instanceof Inet6Address) { continue;// skip ipv6 } String ip = ia.getHostAddress(); if (!"127.0.0.1".equals(ip)) { hostIp = ia.getHostAddress(); break; } } } } catch (SocketException e) { Log.i("error", "SocketException"); e.printStackTrace(); } return hostIp; } /** * 判断地址是否为IPV4地址 */ public static boolean IsIpv4(String ipv4) { if (ipv4 == null || ipv4.length() == 0) { return false;//字符串为空或者空串 } String[] parts = ipv4.split("\\.");//因为java doc里已经说明, split的参数是reg, 即正则表达式, 如果用"|"分割, 则需使用"\\|" if (parts.length != 4) { return false;//分割开的数组根本就不是4个数字 } for (String part : parts) { try { int n = Integer.parseInt(part); if (n < 0 || n > 255) { return false;//数字不在正确范围内 } } catch (NumberFormatException e) { return false;//转换数字不正确 } } return true; } }
(10)ToastUtil.java
import android.content.Context; import android.widget.Toast; public class ToastUtil { private static Toast mToast = null; /** * Toast方法 * * @param text 需要展示的文本 * @param context 所需上下文 */ public static void showToast(Context context, String text) { if (text != null) { if (mToast == null) { mToast = Toast.makeText(context, text, Toast.LENGTH_SHORT); } else { mToast.setText(text); mToast.setDuration(Toast.LENGTH_SHORT); } mToast.show(); } } }
3.权限及声明
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <!--service部分--> <service android:name="com.test.test.LocalService"/>
代码到此为止了,功能比较简单。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。