上篇博客介绍了Socket Socket的基本讲解以及对于内部的方法使用做了一些简单的整理,并且通过ServerSocket自己做了一个通过服务端(PC主机)与多台手机进行通信的Demo,实现了群发功能、指定手机发送消息功能、显示已连接的手机数量以及IP地址和端口号,通过这些功能的实现,就可以实现无线群控功能的一个雏形,自己也有想过能否通过ServerSocket来实现模拟聊天功能的一个Demo,所以就趁热打铁做了一个简易聊天室的App,Server充当终端服务器,来起到接收消息并把消息转发给其它设备,因为真正的聊天功能都是携带具体消息和一个指定客户的地址等信息,来起到客户将消息发送给特定的一个好友,而不是发给了不该收到的人,但是时间有限就只写了一个类似于微信群或是qq讨论组的这样一个聊天功能,希望能够加深自己对ServerSocket的认识了解,同时也能够帮助他人。
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(); } }
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,因为公司需要所以想自学一下,要是有什么问题或是总结都会以博客的形式来见证自己的成长,欢迎大家对我提建议和批评我会一一查看并回复的。