安卓基於Socket通信(伺服器配合) 1.話不多說進入正題,先創建服務端,在Android Studio中創建Java代碼,如下圖所示: 選擇Java Library 需要改名字的自己隨意 2.創建Client Manager客戶端管理類來管理客戶端的消息,因為省時間就直接從我上篇博客的代碼基礎上 ...
安卓基於Socket通信(伺服器配合)
1.話不多說進入正題,先創建服務端,在Android Studio中創建Java代碼,如下圖所示:
選擇Java Library 需要改名字的自己隨意
2.創建Client Manager客戶端管理類來管理客戶端的消息,因為省時間就直接從我上篇博客的代碼基礎上進行的修改~代碼如下所示:(自己編寫代碼塊提交後總有亂碼...所以只能把自己的代碼複製粘貼進來啦~格式有點奇怪,但是沒有亂碼~)
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; /** * Created by sp01 on 2017/4/28. */ // 客戶端的管理類 public class ClientManager { private static Map<String,Socket> clientList = new HashMap<>(); private static ServerThread serverThread = null; private static class ServerThread implements Runnable { private int port = 10010; private boolean isExit = false; private ServerSocket server; public ServerThread() { try { server = new ServerSocket(port); System.out.println("啟動服務成功" + "port:" + port); } catch (IOException e) { System.out.println("啟動server失敗,錯誤原因:" + e.getMessage()); } } @Override public void run() { try { while (!isExit) { // 進入等待環節 System.out.println("等待手機的連接... ... "); final Socket socket = server.accept(); // 獲取手機連接的地址及埠號 final String address = socket.getRemoteSocketAddress().toString(); System.out.println("連接成功,連接的手機為:" + address); new Thread(new Runnable() { @Override public void run() { try { // 單線程索鎖 synchronized (this){ // 放進到Map中保存 clientList.put(address,socket); } // 定義輸入流 InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) != -1){ String text = new String(buffer,0,len); System.out.println("收到的數據為:" + text); // 在這裡群發消息 sendMsgAll(text); } }catch (Exception e){ System.out.println("錯誤信息為:" + e.getMessage()); }finally { synchronized (this){ System.out.println("關閉鏈接:" + address); clientList.remove(address); } } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } public void Stop(){ isExit = true; if (server != null){ try { server.close(); System.out.println("已關閉server"); } catch (IOException e) { e.printStackTrace(); } } } } public static ServerThread startServer(){ System.out.println("開啟服務"); if (serverThread != null){ showDown(); } serverThread = new ServerThread(); new Thread(serverThread).start(); System.out.println("開啟服務成功"); return serverThread; } // 關閉所有server socket 和 清空Map public static void showDown(){ for (Socket socket : clientList.values()) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } serverThread.Stop(); clientList.clear(); } // 群發的方法 public static boolean sendMsgAll(String msg){ try { for (Socket socket : clientList.values()) { OutputStream outputStream = socket.getOutputStream(); outputStream.write(msg.getBytes("utf-8")); } return true; }catch (Exception e){ e.printStackTrace(); } return false; } }
代碼看起來比較簡單,用了儘可能方便理解的書寫,也寫好了一些註釋,應該不難理解所以就不具體解釋了,對Server Socket有不理解的地方,請參考我的上篇博客~希望能有所幫助,但需要解釋的地方可能只有一點吧,群發的方法對收到的消息全部進行廣播式的發送,那麼不就發送的人也會收到消息了嘛?(可能有人感覺會有數據顯示重覆的情況)我想說的是,真正歷史記錄都會在服務端進行數據保存和處理這樣想就行了,我在Android端做了一個RecyclerView的載入不同行佈局實現模擬聊天界面,發送和接收的歷史消息都會顯示在列表上,本人發送的內容在左側,其他人發送的消息被顯示在右側。
3.在MaClass.java(主入口類)中開啟服務:
public class MyClass { public static void main(String[]args){ // 開啟伺服器 ClientManager.startServer(); } }
4.到這裡為止,服務端的代碼就完成了很簡單,有人運行代碼時,會出現控制臺中文亂碼情況,解決辦法我轉發的博客中有介紹,但是考慮到有人很懶,不想在麻煩去找,我就直接在下麵介紹了,很簡單隻需要一句話:
tasks.withType(JavaCompile) { options.encoding = "UTF-8" }
複製代碼塊,放進藍色的gradle位置中(Java lib包內)dependencies{}下方位置,在Rebuild一下就好了。
5.新建並編寫Android客戶端工程,大致內容就是一個EditText輸入框,點擊按鈕發送數據,上方為一個載入不同行佈局的RecyclerView,實現歷史記錄閱覽,下麵是activity_main.xml的內容:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="sq.test_socketchat.MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/rv" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="9"/> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <EditText android:id="@+id/et" android:layout_weight="8" android:layout_width="0dp" android:hint="輸入內容" android:layout_height="match_parent" /> <Button android:id="@+id/btn" android:text="發送" android:layout_margin="3dp" android:layout_weight="2" android:layout_width="0dp" android:layout_height="match_parent" /> </LinearLayout> </LinearLayout>
顯示效果如上圖所示。
6.接下來是準備工作,首先寫一個MyBean,用來存儲名字,消息內容,消息時間,以及載入哪種佈局:
/** * Created by sp01 on 2017/4/28. */ public class MyBean { private String data; private String time,name; private int number; public MyBean() { } public MyBean(String data, int number,String time,String name) { this.data = data; this.number = number; this.name = name; this.time = time; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getData() { return data; } public void setData(String data) { this.data = data; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } }
7.同樣是準備工作,兩個不同佈局的item的書寫,第一種內容顯示在左側第二種則在右側,直接複製我的就好:
第一個item:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:background="#c8fffa" android:layout_margin="5dp" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/tv" android:layout_gravity="left" android:textSize="20sp" android:text="lalala" android:layout_margin="5dp" android:textColor="#000000" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <LinearLayout android:layout_gravity="left" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/tv_name" android:text="name_xx" android:layout_margin="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/tv_time" android:layout_margin="5dp" android:text="1993-09-28" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout> 第二個item: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:background="#fcfdd9" android:layout_margin="5dp" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/tv2" android:layout_gravity="right" android:textSize="20sp" android:text="lalala" android:layout_margin="5dp" android:textColor="#000000" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <LinearLayout android:layout_gravity="right" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/tv_name2" android:text="name_xx" android:layout_margin="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/tv_time2" android:layout_margin="5dp" android:text="1993-09-28" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
8.接下來是書寫MyAdapter內的代碼(RecyclerView載入不同行佈局很簡單就不過多強調了):
import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.ArrayList; /** * Created by sp01 on 2017/4/28. */ public class MyAdapter extends RecyclerView.Adapter { private Context context; private ArrayList<MyBean> data; private static final int TYPEONE = 1; private static final int TYPETWO = 2; public MyAdapter(Context context) { this.context = context; } public void setData(ArrayList<MyBean> data) { this.data = data; notifyDataSetChanged(); } @Override public int getItemViewType(int position) { return data.get(position).getNumber(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RecyclerView.ViewHolder holder = null; switch (viewType){ case TYPEONE: View view = LayoutInflater.from(context).inflate(R.layout.item,parent,false); holder = new OneViewHolder(view); break; case TYPETWO: View view1 = LayoutInflater.from(context).inflate(R.layout.item2,parent,false); holder = new TwoViewHolder(view1); break; } return holder; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { int itemViewType = getItemViewType(position); switch (itemViewType){ case TYPEONE: OneViewHolder oneViewHolder = (OneViewHolder) holder; oneViewHolder.tv1.setText(data.get(position).getData()); oneViewHolder.name1.setText(data.get(position).getName()); oneViewHolder.time1.setText(data.get(position).getTime()); break; case TYPETWO: TwoViewHolder twoViewHolder = (TwoViewHolder) holder; twoViewHolder.tv2.setText(data.get(position).getData()); twoViewHolder.name2.setText(data.get(position).getName()); twoViewHolder.time2.setText(data.get(position).getTime()); break; } } @Override public int getItemCount() { return data != null && data.size() > 0 ? data.size() : 0; } class OneViewHolder extends RecyclerView.ViewHolder{ private TextView tv1; private TextView name1,time1; public OneViewHolder(View itemView) { super(itemView); tv1 = (TextView) itemView.findViewById(R.id.tv); name1 = (TextView) itemView.findViewById(R.id.tv_name); time1 = (TextView) itemView.findViewById(R.id.tv_time); } } class TwoViewHolder extends RecyclerView.ViewHolder{ private TextView tv2; private TextView name2,time2; public TwoViewHolder(View itemView) { super(itemView); tv2 = (TextView) itemView.findViewById(R.id.tv2); name2 = (TextView) itemView.findViewById(R.id.tv_name2); time2 = (TextView) itemView.findViewById(R.id.tv_time2); } } }
9.下麵終於進入到了正題~進入到MainActivity中,代碼如下所示:
import android.os.Handler; import android.os.Message; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.Button; import android.widget.EditText; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; public class MainActivity extends AppCompatActivity { private RecyclerView rv; private EditText et; private Button btn; private Socket socket; private ArrayList<MyBean> list; private MyAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); rv = (RecyclerView) findViewById(R.id.rv); et = (EditText) findViewById(R.id.et); btn = (Button) findViewById(R.id.btn); list = new ArrayList<>(); adapter = new MyAdapter(this); final Handler handler = new MyHandler(); new Thread(new Runnable() { @Override public void run() { try { socket = new Socket("192.168.1.111", 10010); InputStream inputStream = socket.getInputStream(); byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) != -1) { String data = new String(buffer, 0, len); // 發到主線程中 收到的數據 Message message = Message.obtain(); message.what = 1; message.obj = data; handler.sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } } }).start(); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final String data = et.getText().toString(); new Thread(new Runnable() { @Override public void run() { try { OutputStream outputStream = socket.getOutputStream(); SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss"); //設置日期格式 outputStream.write((socket.getLocalPort() + "//" + data + "//" + df.format(new Date())).getBytes("utf-8")); outputStream.flush(); } catch (IOException e) { e.printStackTrace(); } } }).start(); } }); } private class MyHandler extends Handler { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.what == 1) { // int localPort = socket.getLocalPort(); String[] split = ((String) msg.obj).split("//"); if (split[0].equals(localPort + "")) { MyBean bean = new MyBean(split[1],1,split[2],"我:"); list.add(bean); } else { MyBean bean = new MyBean(split[1],2,split[2],("來自:" + split[0])); list.add(bean); } // 向適配器set數據 adapter.setData(list); rv.setAdapter(adapter); LinearLayoutManager manager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false); rv.setLayoutManager(manager); } } } }
代碼很簡單,因為Socket發送的數據只能是一個基本的數據類型,不能傳遞類似於HashMap、集合、數組這樣的數據,所以只能通過拼接字元串的形式通過加入一些特殊的符號,來起到分割數據的作用,因為傳遞的數據中帶有發送者、接受者、時間、消息等這樣的數據,所以通過split來區別這些數據,從而進行具體的分配來實現目的。
10.最後許可權不要忘記加入~
<uses-permission android:name="android.permission.INTERNET"/>
那麼運行實現的具體效果又是怎樣的呢?請看下麵(話說CSDN加圖好麻煩啊):
1)這是開啟伺服器之後,兩台手機打開聊天Demo:
2)這是客戶端發送數據的顯示內容:
3)這是服務端在客戶端聊天時顯示的Log:
Demo:https://github.com/shiqiangdva/Socket_Chat
--------------------轉載至https://blog.csdn.net/qq_37842258/article/details/70945192