安卓基于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:
总结:在这篇博客中,主要是对ServerSocket和Android相结合的应用场景进行了自己为是的分析,觉得聊天的通信方式可以通过Socket来实现,但是本文没有对ServerSocket的基础部分进行大量的讲解,因为在我的上篇博客中已经进行的很多的讲解和分析了,所以就不做过多的累赘的去写重复性的东西了,本文的一切东西都是自己亲自手写手敲的,如果这两篇关于socket的讲解和Demo为你带来了哪怕仅仅一点点微不足道的帮助,都希望给我留个言点个赞来让我知道,浪费不了你几秒钟的时间~~虽然自己也是在最近才接触的Socket但是有了一点心得,就总结了这两篇博客,写了俩个Demo来分享自己的一些浅薄的经验,如果需要下载我的项目请 <点击这里> 进行下载,里面有个下载后先进行阅读的文档要先查看,两个项目我都放在一个大包中了所以不需要下载两次。下篇可能会介绍线程池,我日后也有打算自学.net,因为公司需要所以想自学一下,要是有什么问题或是总结都会以博客的形式来见证自己的成长,欢迎大家对我提建议和批评我会一一查看并回复的。
转载不易,打赏一下呗:]
--------------------转载至https://blog.csdn.net/qq_37842258/article/details/70945192