使用 Android、Node.js 和 Socket.io 创建一个实时聊天应用
介绍
入门
我们的Node.js服务器
我们的安卓应用(Socket客户端)
结论
介绍
WebSocket 是一种非常强大的工具,它允许我们在现代 Web 应用程序中建立实时通信。事实上,这种机制非常强大,被用于构建各种类型的应用程序,例如实时聊天或通知系统等等。
本文将向您展示如何使用 Android Node.js 和 Socket.io 构建实时聊天应用程序。
入门
我们的聊天应用程序分为两部分:
1. 服务器端:一个使用 Node.js 并实现了 socket.io 的服务器端。
2- 客户端:创建安卓应用并为客户端实现 socket.io。
我们的Node.js服务器
为了更清楚地说明,我们的项目架构将由两个文件组成:
package.json,它将处理我们 node js 应用程序的所有依赖项;index.js,它将是我们的主服务器。
创建完这两个文件后,我们在项目目录下打开命令行
并执行以下命令
npm install --save express socket.io
现在,在 index.js 文件中,我们将构建服务器并进行所有配置,使其看起来像这样。
const express = require('express'),
http = require('http'),
app = express(),
server = http.createServer(app),
io = require('socket.io').listen(server);
app.get('/', (req, res) => {
res.send('Chat Server is running on port 3000')
});
server.listen(3000,()=>{
console.log('Node app is running on port 3000')
});
为确保服务器正在运行,请进入项目目录下的命令行并执行以下命令
node index.js
注意:虽然可以使用 node 命令运行任何用 node 环境创建的服务器,但问题在于每次更新 index.js 文件时都必须运行相同的命令。为了简化操作,我们可以使用 nodemon 命令,它会在每次更改后自动重启服务器。
所以要安装 nodemon,请打开命令行并运行
npm install -g nodemon
为了确保项目正在运行,我们应该在控制台中看到以下日志。
现在到了最精彩的部分!!
现在我们将尝试在服务器中实现一些 socket.io 方法,以处理聊天应用程序的所有事件,包括用户的连接状态和消息。
在我们的 index.js 文件中,我们添加了第一个实现,用于检测是否有用户连接到我们的服务器。
io.on('connection', (socket) => {
console.log('user connected')
socket.on('join', function(userNickname) {
console.log(userNickname +" : has joined the chat " )
socket.broadcast.emit('userjoinedthechat',userNickname +" : has joined the chat ")
});
});
实际上,socket.io 机制是基于监听和触发事件的。在我们完成的第一个实现中,(on) 方法接受两个参数('eventname',callback),定义了一个名为 connection 的事件的监听器。该事件将从客户端触发,以便 node js 可以处理它。之后,我们定义了一个方法,该方法将监听名为 'join' 的已发出事件,并将加入聊天的用户的姓名记录到控制台中。
现在,当 Node.js 检测到用户时,它会使用 emit 方法向客户端触发一个名为“userjoinedthechat”的事件,请注意 socket.broadcast.emit 会将事件发送给除发送者之外连接到服务器的每个用户。
如果我们想将消息发送给包括发件人在内的所有用户,我们只需要使用 io.emit() 而不是 socket.emit()。
现在为了处理消息,我们添加以下几行代码,可以看到我们向回调函数添加了额外的参数,即用户昵称和消息内容。实际上,这些信息将在触发“messagedetection”事件时从客户端发送。
socket.on('messagedetection', (senderNickname,messageContent) => {
//log the message in console
console.log(senderNickname+" :" +messageContent)
//create a message object
let message = {"message":messageContent, "senderNickname":senderNickname}
// send the message to the client side
socket.emit('message', message )
});
最后,当用户与客户端断开连接时,该事件将由该实现处理。
socket.on('disconnect', function() {
console.log( 'user has left ')
socket.broadcast.emit( "userdisconnect" ,' user has left')
});
服务器准备就绪后,index.js 文件应该如下所示:
const express = require('express'),
http = require('http'),
app = express(),
server = http.createServer(app),
io = require('socket.io').listen(server);
app.get('/', (req, res) => {
res.send('Chat Server is running on port 3000')
});
io.on('connection', (socket) => {
console.log('user connected')
socket.on('join', function(userNickname) {
console.log(userNickname +" : has joined the chat " );
socket.broadcast.emit('userjoinedthechat',userNickname +" : has joined the chat ");
})
socket.on('messagedetection', (senderNickname,messageContent) => {
//log the message in console
console.log(senderNickname+" : " +messageContent)
//create a message object
let message = {"message":messageContent, "senderNickname":senderNickname}
// send the message to all users including the sender using io.emit()
io.emit('message', message )
})
socket.on('disconnect', function() {
console.log(userNickname +' has left ')
socket.broadcast.emit( "userdisconnect" ,' user has left')
})
})
server.listen(3000,()=>{
console.log('Node app is running on port 3000')
})
我们的安卓应用(Socket客户端)
首先打开 Android Studio 并创建一个包含空 Activity 的新项目,然后打开 app 的 build.gradle 文件并添加这些依赖项,最后同步你的项目。
compile 'com.android.support:recyclerview-v7:25.3.1'
compile('com.github.nkzawa:socket.io-client:0.5.0') {
exclude group: 'org.json', module: 'json'
}
现在来说说这些句子:
第一个是 RecyclerView,我们将使用它来显示消息列表;第二个是库,它将为我们提供客户端 socket.io 的实现,以便我们可以触发或监听事件。
别忘了在 manifest.xml 文件中启用 INTERNET 权限。
<uses-permission android:name="android.permission.INTERNET" ></uses-permission>
在 activity_main.xml 文件中,我们将添加一个 EditText 控件供用户输入昵称,以及一个按钮,允许用户进入聊天框。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.aymen.androidchat.MainActivity">
<EditText
android:id="@+id/nickname"android:layout_centerInParent="true"android:textSize="30dp"android:hint="Enter your nickname !"android:layout_width="match_parent"android:layout_height="wrap_content" /><Buttonandroid:layout_below="@+id/nickname"android:id="@+id/enterchat"android:text="Go to chat "android:layout_width="match_parent"android:layout_height="wrap_content" />
</RelativeLayout>
现在你的 MainActivity.java 文件应该看起来像这样
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private Button btn;
private EditText nickname;
public static final String NICKNAME = "usernickname";
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//call UI components by id
btn = (Button)findViewById(R.id.enterchat) ;
nickname = (EditText) findViewById(R.id.nickname);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//if the nickname is not empty go to chatbox activity and add the nickname to the intent extra
if(!nickname.getText().toString().isEmpty()){
Intent i = new Intent(MainActivity.this,ChatBoxActivity.class);
//retreive nickname from EditText and add it to intent extra
i.putExtra(NICKNAME,nickname.getText().toString());
startActivity(i);
}
}
});
}
}
现在创建第二个名为 ChatBoxActivity 的空 Activity,并在 activity_chat_box.xml 文件中添加以下几行代码。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.aymen.androidchat.ChatBoxActivity">
<LinearLayout
android:weightSum="3"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerViewandroid:layout_weight="3"android:id="@+id/messagelist"android:layout_width="match_parent"android:layout_height="wrap_content"android:clipToPadding="false"android:scrollbars="vertical"/><Viewandroid:layout_marginTop="5mm"android:id="@+id/separator"android:layout_width="match_parent"android:layout_height="1dp"android:background="@android:color/darker_gray"/>
<LinearLayoutandroid:weightSum="3"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content">
<EditText
android:id="@+id/message"android:layout_weight="3"android:layout_width="wrap_content"android:hint="your message"
android:layout_height="match_parent" />
<Button
android:id="@+id/send"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="#00000000"android:text="send"
/></LinearLayout>
</LinearLayout>
</RelativeLayout>
在实现套接字客户端之前,我们应该创建一个适配器来处理和显示我们的消息。为此,我们需要创建一个名为 item.xml 的文件和一个名为 message 的 Java 类,它有两个简单的字符串属性(nickname,message)。
在项目目录下的 activities 文件夹中,创建一个名为 Message.java 的文件:
public class Message {
private String nickname;
private String message ;
public Message(){
}
public Message(String nickname, String message) {
this.nickname = nickname;
this.message = message;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
现在在 layout 目录下创建一个名为 item.xml 的文件,并添加以下几行代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal" android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@id/nickname"android:textSize="15dp"android:textStyle="bold"android:text="Nickname : "android:layout_width="wrap_content"android:layout_height="wrap_content" /><TextViewandroid:id="@id/message"android:textSize="15dp"android:text=" message "android:layout_width="wrap_content"android:layout_height="wrap_content" />
</LinearLayout>
创建一个名为 ChatBoxAdapter.java 的文件,并将以下代码放入其中。
package com.example.aymen.androidchat;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
public class ChatBoxAdapter extends RecyclerView.Adapter<ChatBoxAdapter.MyViewHolder> {
private List<Message> MessageList;
public class MyViewHolder extends RecyclerView.ViewHolder {
public TextView nickname;
public TextView message;
public MyViewHolder(View view) {
super(view);
nickname = (TextView) view.findViewById(R.id.nickname);
message = (TextView) view.findViewById(R.id.message);
}
}
// in this adaper constructor we add the list of messages as a parameter so that
// we will passe it when making an instance of the adapter object in our activity
public ChatBoxAdapter(List<Message>MessagesList) {
this.MessageList = MessagesList;
}
@Overridepublic int getItemCount() {
return MessageList.size();
}
@Overridepublic ChatBoxAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item, parent, false);
return new ChatBoxAdapter.MyViewHolder(itemView);
}
@Overridepublic void onBindViewHolder(final ChatBoxAdapter.MyViewHolder holder, final int position) {
//binding the data from our ArrayList of object to the item.xml using the viewholder
Message m = MessageList.get(position);
holder.nickname.setText(m.getNickname());
holder.message.setText(m.getMessage() );
}
}
现在一切准备就绪,我们可以在 ChatBoxActivity.java 中实现套接字客户端,具体步骤如下:
1. 从 intent extra 中获取用户的昵称
2.调用并实现与 RecyclerView 相关的所有方法,包括适配器实例化。
3.声明并定义套接字客户端与服务器建立连接的主机。
4.处理服务器触发的所有事件
5.当用户连接、断开连接或发送消息时发出事件
但在此之前,让我们先检查一下一切是否正常。因此,在我们的 ChatBoxActivity 中,我们将声明 socket 对象,并在 onCreate 方法中添加 socket 连接,这样当 Activity 被调用时,socket 客户端将直接触发连接事件。
public class ChatBoxActivity extends AppCompatActivity {
//declare socket object
private Socket socket;
private String Nickname ;
@Overrideprotected
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_box);
// get the nickame of the user
Nickname= (String)getIntent().getExtras().getString(MainActivity.NICKNAME);
//connect you socket client to the server
try {
//if you are using a phone device you should connect to same local network as your laptop and disable your pubic firewall as well
socket = IO.socket("http://yourlocalIPaddress:3000");
//create connection
socket.connect()
// emit the event join along side with the nickname
socket.emit('join',Nickname);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
现在运行你的模拟器,在第一个活动中输入一个昵称,然后点击“开始聊天”。你会在服务器控制台中看到一条日志,表明用户已成功连接到服务器。我们可以看到,服务器中触发的 join 事件监听器工作正常,能够记录已连接用户的姓名。
现在一切正常,我们不应该忘记,当我们的服务器处理一个事件时,它也会广播其他自定义事件,因此这些触发的事件应该在客户端处理。为此,我们将为事件“userjoinedthechat”创建第一个监听器,这是一个在服务器处理事件“join”时触发的自定义事件。
在我们的 ChatBoxActivity 中,我们将添加以下代码行
socket.on("userjoinedthechat", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
// get the extra data from the fired event and display a toast
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
现在我们同时运行两个模拟器,分别从两端输入不同的昵称,可以看到其中一个模拟器显示有用户已成功加入聊天。
现在到了我们应用最精彩的部分——聊天信息:
要显示这些消息,我们必须按以下步骤操作。
1. 为发送按钮添加 onclickListener,并在获取 EditText 中的消息内容后,使用 emit() 方法发出“messagedetection”事件,同时传递发送者的昵称和消息内容。
2.该事件将由服务器处理并广播给所有用户。
3. 在 Android 中添加一个套接字监听器,用于监听服务器触发的“message”事件。
4. 从额外数据中提取昵称和消息,并创建一个新的 Message 对象实例。
5. 将实例添加到消息 ArrayList 中,并通知适配器更新 RecyclerView。
但在此之前,让我们先设置一下回收站视图、适配器、消息文本字段和发送按钮。
在 ChatBoxActivity 中添加以下声明
public RecyclerView myRecylerView ;
public List<Message> MessageList ;
public ChatBoxAdapter chatBoxAdapter;
public EditText messagetxt ;
public Button send ;
在 onCreate 方法中添加以下几行
messagetxt = (EditText) findViewById(R.id.message) ;
send = (Button)findViewById(R.id.send);
MessageList = new ArrayList<>();
myRecylerView = (RecyclerView) findViewById(R.id.messagelist);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
myRecylerView.setLayoutManager(mLayoutManager);
myRecylerView.setItemAnimator(new DefaultItemAnimator());
现在,在你的 ChatBoxActivity 中,按钮操作应该如下所示:
send.setOnClickListener(new View.OnClickListener() {
@Overridepublic void onClick(View v) {
//retrieve the nickname and the message content and fire the event messagedetection
if(!messagetxt.getText().toString().isEmpty()){
socket.emit("messagedetection",Nickname,messagetxt.getText().toString());
messagetxt.setText(" ");
}
}
});
监听器应该看起来像这样
socket.on("message", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
JSONObject data = (JSONObject) args[0];
try {
//extract data from fired event
String nickname = data.getString("senderNickname");
String message = data.getString("message");
// make instance of message
Message m = new Message(nickname,message);
//add the message to the messageList
MessageList.add(m);
// add the new updated list to the adapter
chatBoxAdapter = new ChatBoxAdapter(MessageList);
// notify the adapter to update the recycler view
chatBoxAdapter.notifyDataSetChanged();
//set the adapter for the recycler view
myRecylerView.setAdapter(chatBoxAdapter);
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
});
如下面的截图所示,一切运行正常:))双方都能显示消息。请注意,我们可以与其他许多用户连接,但只需运行其他模拟器并输入昵称即可加入聊天室。
在本教程结束之前,我们还需要实现最后一个功能,即检测用户是否已与聊天框断开连接。
在我们的 ChatBoxActivity 中重写 onDestroy() 方法并添加以下代码行
@Override
protected void onDestroy() {
super.onDestroy();
socket.disconnect();
}
对于听众而言
socket.on("userdisconnect", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
});
最终,我们的 ChatBoxActivity 将如下所示。
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.DefaultItemAnimator;
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 android.widget.Toast;
import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
public class ChatBoxActivity extends AppCompatActivity {
public RecyclerView myRecylerView ;
public List<Message> MessageList ;
public ChatBoxAdapter chatBoxAdapter;
public EditText messagetxt ;
public Button send ;
//declare socket objectprivate Socket socket;
public String Nickname ;
@Overrideprotected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_box);
messagetxt = (EditText) findViewById(R.id.message) ;
send = (Button)findViewById(R.id.send);
// get the nickame of the user
Nickname= (String)getIntent().getExtras().getString(MainActivity.NICKNAME);
//connect you socket client to the servertry {
socket = IO.socket("http://yourlocalIPaddress:3000");
socket.connect();
socket.emit("join", Nickname);
} catch (URISyntaxException e) {
e.printStackTrace();
}
//setting up recyler
MessageList = new ArrayList<>();
myRecylerView = (RecyclerView) findViewById(R.id.messagelist);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
myRecylerView.setLayoutManager(mLayoutManager);
myRecylerView.setItemAnimator(new DefaultItemAnimator());
// message send action
send.setOnClickListener(new View.OnClickListener() {
@Overridepublic void onClick(View v) {
//retrieve the nickname and the message content and fire the event messagedetectionif(!messagetxt.getText().toString().isEmpty()){
socket.emit("messagedetection",Nickname,messagetxt.getText().toString());
messagetxt.setText(" ");
}
}
});
//implementing socket listeners
socket.on("userjoinedthechat", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
});
socket.on("userdisconnect", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
String data = (String) args[0];
Toast.makeText(ChatBoxActivity.this,data,Toast.LENGTH_SHORT).show();
}
});
}
});
socket.on("message", new Emitter.Listener() {
@Overridepublic void call(final Object... args) {
runOnUiThread(new Runnable() {
@Overridepublic void run() {
JSONObject data = (JSONObject) args[0];
try {
//extract data from fired event
String nickname = data.getString("senderNickname");
String message = data.getString("message");
// make instance of message
Message m = new Message(nickname,message);
//add the message to the messageList
MessageList.add(m);
// add the new updated list to the dapter
chatBoxAdapter = new ChatBoxAdapter(MessageList);
// notify the adapter to update the recycler view
chatBoxAdapter.notifyDataSetChanged();
//set the adapter for the recycler view
myRecylerView.setAdapter(chatBoxAdapter);
} catch (JSONException e) {
e.printStackTrace();
}
}
});
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
socket.disconnect();
}
}
结论
在这个例子中,我们深入了解了 socket.io 与 node js 和 android 的结合使用,我们也尝试解释一些基础知识,理解 socket.io 的机制以及如何在客户端和服务器之间建立双向通信。请注意,socket.io 中还有其他工具,例如 rooms 和 namespaces,它们对于创建漂亮的 Web 和移动应用程序非常有帮助。
在以下相关链接中查找这两个项目:
客户端:https://github.com/medaymenTN/AndroidChat
服务器端:https://github.com/medaymenTN/NodeJSChatServer
文章来源:https://dev.to/medaymentn/creating-a-realtime-chat-app-with-android--nodejs-and-socketio-4o55