一、最近,学习了安卓socket通信。

刚好手上又需要一个客服聊天的功能,可惜这个demo最终还是不太符合我项目的要求,因此,今天先写下来供以后复习使用。

之所以选择socket是因为网上推送,IM等SDK有很多,不仅要收费,用户信息那些都要存到他们的平台,不太安全。

二、此demo已实现功能:

1、多台手机进行连接,可选择对应的id号进行聊天。

2、安卓聊天界面的实现。

三、还想实现的功能但仍未实现的功能:

1、离线消息。

2、聊天记录保存到手机数据库。

3、通过socket id绑定用户账号,当某用户发信息过来的时候,可以通过用户来得到对应的socket id,从而把消息转发出去。(这一个我是真的想不到,学业不精啊,求赐教!)。

四、demo效果:

安卓socket一对一聊天小demo_android

安卓socket一对一聊天小demo_android_02

五、原理:

当每有一台手机连接到服务器时,会对该手机生成一个线程和生成一个socket id,手机之间可以通过对应的socket id和目标手机聊天。

六、功能实现:

1、服务器的实现(eclipse软件,引用了一个json.jar包)

SocketMessage.java

import com.jimstin.server.MyServer.SocketThread;
public class SocketMessage {

public int to;
public int from;
public String msg;
public String time;
public SocketThread thread;
public String username;
public String user;
}

MyServer.java

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import org.json.JSONObject;
import com.jimstin.msg.SocketMessage;

public class MyServer {

private boolean isStartServer;
private ServerSocket mServer;

private ArrayList<SocketMessage> mMsgList = new ArrayList<SocketMessage>();
private ArrayList<SocketThread> mThreadList = new ArrayList<SocketThread>();
/**
* 开启服务器
*/
private void startSocket() {
try {
isStartServer = true;
int port = 2000;
mServer = new ServerSocket(port);
System.out.println("启动server,端口:"+port);
Socket socket = null;
int socketID = 0;
startMessageThread();
while(isStartServer) {
socket = mServer.accept();
SocketThread thread = new SocketThread(socket, socketID++);
thread.start();
mThreadList.add(thread);
}

} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 转发消息
*/
public void startMessageThread() {
new Thread(){
@Override
public void run() {
super.run();
try {
while(isStartServer) {
if(mMsgList.size() > 0) {
SocketMessage from = mMsgList.get(0);
for(SocketThread to : mThreadList) {
if(to.socketID == from.to) {
BufferedWriter writer = to.writer;
JSONObject json = new JSONObject();
json.put("from", from.from);
json.put("msg", from.msg);
json.put("time", from.time);
writer.write(json.toString()+"\n");
writer.flush();
System.out.println("转发消息成功:"+from.msg+">> to socketID:"+from.to);
break;
}
}
mMsgList.remove(0);
}
Thread.sleep(200);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
/**
* 接收来自客户端的消息
* json数组包括socketID,msg
* @author Administrator
*
*/
public class SocketThread extends Thread {

public int socketID;
public Socket socket;
public BufferedWriter writer;
public BufferedReader reader;

public SocketThread(Socket socket, int count) {
socketID = count;
this.socket = socket;
System.out.println("新增一台客户机,socketID:"+socketID);

}

@Override
public void run() {
super.run();

try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "utf-8"));
while(isStartServer) {
if(reader.ready()) {
String data = reader.readLine();
JSONObject json = new JSONObject(data);
SocketMessage msg = new SocketMessage();
msg.to = json.getInt("to");
msg.msg = json.getString("msg");

msg.from = socketID;

msg.time = getTime(System.currentTimeMillis());
mMsgList.add(msg);
System.out.println("收到一条消息:"+json.getString("msg")+" >>>> to socketID:"+json.getInt("to"));
}
Thread.sleep(100);
}

} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 得到此刻的时间
* @param millTime
* @return
*/
private String getTime(long millTime) {
Date d = new Date(millTime);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(d));
return sdf.format(d);
}
public static void main(String[] args) {
MyServer server = new MyServer();
server.startSocket();
}

}

2、安卓实现

(1)先来看看manifest:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jw.socketclient"
android:versionCode="1"
android:versionName="1.0" >

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.jw.socketclient.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

(2)用到的依赖:

implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'

(3)界面:

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/start_btn"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="start"/>
<Button
android:id="@+id/stop_btn"
android:text="stop"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

<EditText
android:id="@+id/socket_id_edt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="socketID"/>

<androidx.recyclerview.widget.RecyclerView

android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<!--<TextView
android:id="@+id/console_txt"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#E3ECE3"
>
<EditText
android:id="@+id/msg_edt"
android:layout_marginLeft="30dp"
android:layout_marginRight="10dp"
android:layout_width="160dp"
android:layout_height="40dp"
android:background="#ffffff"
android:layout_gravity="center"
android:layout_weight="9"
/>
<Button
android:id="@+id/send_btn"
android:layout_width="40dp"
android:layout_height="35dp"
android:text="发送"
android:textColor="#ffffff"
android:layout_gravity="center"
android:background="#0B9C10"
android:layout_weight="1"/>
</LinearLayout>


</LinearLayout>

item.xml(右边的界面,展示自己发出的消息)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="6dp"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical" >

<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ECE8E8"
android:padding="2dp"
android:textColor="#ffffff"
android:textSize="12sp" />
</LinearLayout>

<RelativeLayout
android:layout_marginTop="5dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<ImageView
android:id="@+id/iv_userhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:background="@drawable/head_img"
android:focusable="false" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/iv_userhead"
android:textColor="#818181"
android:textSize="15sp" />
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_toLeftOf="@id/iv_userhead"
android:clickable="true"
android:focusable="true"
android:lineSpacingExtra="2dp"
android:minHeight="50dp"
android:gravity="center"
android:background="@drawable/chat_to"
android:textColor="#ff000000"
android:textSize="15sp" />
</RelativeLayout>

</LinearLayout>

item2.xml(左边界面,展示别人发过来的消息)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp"
>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical" >

<TextView
android:id="@+id/tv_time2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ECE8E8"
android:padding="2dp"
android:textColor="#ffffff"
android:textSize="12sp" />
</LinearLayout>
<RelativeLayout
android:layout_marginTop="5dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<ImageView
android:id="@+id/iv_userhead2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:background="@drawable/feedback"
android:focusable="false" />
<TextView
android:id="@+id/tv_name2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/iv_userhead2"
android:textColor="#818181"
android:textSize="15sp"
android:layout_alignParentLeft="true"
/>
<TextView
android:id="@+id/tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@id/iv_userhead2"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:lineSpacingExtra="2dp"
android:minHeight="50dp"
android:background="@drawable/chat_from"
android:textColor="#ff000000"
android:textSize="15sp"
/>
</RelativeLayout>
</LinearLayout>

(4)4张图,随便网上下载几张矢量图即可。

安卓socket一对一聊天小demo_ide_03

(5)活动:

MainActivity.java

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import org.json.JSONObject;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.app.Activity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class MainActivity extends Activity implements OnClickListener {

private EditText mSocketIDEdt, mMessageEdt;
private StringBuffer mConsoleStr = new StringBuffer();
private Socket mSocket;
private boolean isStartRecieveMsg;
private RecyclerView rv;
private SocketHandler mHandler;
protected BufferedReader mReader;
protected BufferedWriter mWriter;
private static final int COMPLETED = 1;
private ArrayList<MyBean> list;
private MyAdapter adapter;
Thread thread;
private static boolean flag=false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();


}

private void initView() {

mSocketIDEdt = (EditText) findViewById(R.id.socket_id_edt);
mMessageEdt = (EditText) findViewById(R.id.msg_edt);
findViewById(R.id.start_btn).setOnClickListener(this);
findViewById(R.id.send_btn).setOnClickListener(this);
findViewById(R.id.stop_btn).setOnClickListener(this);
mHandler = new SocketHandler();
rv = (RecyclerView) findViewById(R.id.rv);
list = new ArrayList<>();
adapter = new MyAdapter(this);
rv.setAdapter(adapter);
LinearLayoutManager manager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false);
rv.setLayoutManager(manager);
}

private void initSocket() {
thread = new Thread(new Runnable() {

@Override
public void run() {

try {
isStartRecieveMsg = true;
mSocket = new Socket("192.168.2.172", 2000);
mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream(), "utf-8"));
mWriter = new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream(), "utf-8"));
while(isStartRecieveMsg) {
if(mReader.ready()) {
mHandler.obtainMessage(0, mReader.readLine()).sendToTarget();

}
Thread.sleep(200);
}
mWriter.close();
mReader.close();
mSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();

}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.send_btn:
send();
break;
case R.id.start_btn:
if(!isStartRecieveMsg) {
initSocket();
}
break;
case R.id.stop_btn:
flag=true;
System.err.println("中断");
default:
break;
}
}

private void send() {

new AsyncTask<String, Integer, String>() {

@Override
protected String doInBackground(String... params) {
sendMsg();
return null;
}
}.execute();
}

protected void sendMsg() {
try {
String socketID = mSocketIDEdt.getText().toString().trim();
String msg = mMessageEdt.getText().toString().trim();
JSONObject json = new JSONObject();
json.put("to", socketID);
json.put("msg", msg);
mWriter.write(json.toString()+"\n");
mWriter.flush();
mConsoleStr.append("我" +msg+" "+getTime(System.currentTimeMillis())+"\n");
MyBean bean = new MyBean("我",2,msg,getTime(System.currentTimeMillis()));
list.add(bean);
/**
* 在子线程更新UI
*/
Message message = new Message();
message.what = COMPLETED;
mHandler.sendMessage(message);

} catch (Exception e) {
e.printStackTrace();
}
}

class SocketHandler extends Handler {

@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case 0:
try {
JSONObject json = new JSONObject((String)msg.obj);
MyBean bean = new MyBean(json.getString("from"),1,json.getString("msg"),getTime(System.currentTimeMillis()));
list.add(bean);
} catch (Exception e) {
e.printStackTrace();
}
// 向适配器set数据
adapter.setData(list);
rv.setAdapter(adapter);
LinearLayoutManager manager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false);
rv.setLayoutManager(manager);
rv.scrollToPosition(adapter.getItemCount()-1);//recycleview滑到底部

break;
case 1:

// 向适配器set数据
adapter.setData(list);
rv.setAdapter(adapter);
manager = new LinearLayoutManager(MainActivity.this, LinearLayoutManager.VERTICAL, false);
rv.setLayoutManager(manager);
rv.scrollToPosition(adapter.getItemCount()-1);//recycleview滑到底部

break;
default:
break;
}
}
}

@Override
public void onBackPressed() {
super.onBackPressed();
isStartRecieveMsg = false;
}

private String getTime(long millTime) {
Date d = new Date(millTime);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(d));
return sdf.format(d);
}

}

(6)适配器:

MyAdapter.java

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;

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.item2,parent,false);
holder = new OneViewHolder(view);
break;
case TYPETWO:
View view1 = LayoutInflater.from(context).inflate(R.layout.item,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.tv2.setText(data.get(position).getData());
oneViewHolder.name2.setText(data.get(position).getName());
oneViewHolder.time2.setText(data.get(position).getTime());
break;
case TYPETWO:
TwoViewHolder twoViewHolder = (TwoViewHolder) holder;
twoViewHolder.tv1.setText(data.get(position).getData());
twoViewHolder.name1.setText(data.get(position).getName());
twoViewHolder.time1.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 tv2;
private TextView name2,time2;
public OneViewHolder(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);

}
}

class TwoViewHolder extends RecyclerView.ViewHolder{
private TextView tv1;
private TextView name1,time1;
public TwoViewHolder(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);
}
}
}

MyBean.java

public class MyBean {
private String data;
private String time,name;
private int number;

public MyBean() {
}

public MyBean(String name, int number,String data,String time) {
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)测试效果:

首先在eclipse运行MyServer.java,然后运行安卓模拟器,点击start,往第一个编辑框输入0,往第二个编辑器输入hello,点击发送,模拟器出现两个hello即成功。可以多开几个模拟器试试。

七、源码

源码

提取码:6x59

八、不足之处:

该demo还有很多需要改善的地方,例如没有离线消息功能,没有聊天记录的存储。希望各位大神可以多多指教。