آموزش ساخت چت realtime با اندروید , NodeJs و Socket.io
آموزش ساخت چت realtime با اندروید ، NodeJs و Socket.io ، عنوان آموزشی می باشد که در این ساعت از تجاری اپ به شما اموزش خواهیم داد.با ما همراه باشید.
استفاده از چت در خیلی از برنامه های موبایل مرسوم و جذاب است ! و عمده جذابیت آن نیز به این دلیل است که می توانید به صورت آنی و real time با مخاطب به صورت متن ، صدا یا تصویر در ارتباط باشید.در این مقاله به صورت گام به گام ساخت چت realtime با اندروید ، NodeJs و Socket.io را به شما آموزش خواهیم داد.
قطعا Socket.io. اما از سوکت صرفا برای چت استفاده نمی شود، سوکت ابزار بسیار قدرتمند و جذابی ست که یک رابطه real time را برای ما فراهم می کند و ما می توانیم در بستر این رابطه عملیاتی اعم از چت یا سیستم نوتیفیکیشن یا هرچیز دیگری که نیاز به رابطه آنی یا real time دارد داشته باشیم.البته سوکت را می توان در زبان ها و پلتفرم های مختلف استفاده کرد مانند جاوا و کاتلین برای اندروید یا nodeJs و PHP برای سمت وب.
در این پست از تجاری اپ ما یک سیستم چت real time برای پلتفرم android با استفاده از nodeJs و Socket.io پیاده میکنیم.
خب مانند همه اپلیکیشن های آنلاین، اپلیکیشن چت اندرویدی که الان قصد داریم باهم پیاده کنیم هم دارای دو بخش خواهد بود: سمت Backend و سمت Frontend.
ما برای سمت Backend پروژه ساخت چت با اندروید از زبان Node js استفاده می کنیم . این درحالی ست که می توان از زبان های دیگر مانند php هم استفاده کرد اما php با Node js استثنا برای اپلیکیشن چت اصلا قابل مقایسه نیستند چرا که Node js با سرعت بسیار بیشتری نسبت PHP برای کار های آنی یا Real time عمل می کند.
توجه:
در هردو سمت سرور و کلاینت ما کتابخانه Socket.io متناسب با هر زبان ( زبان Node js برای سمت Backend و زبان کاتلین یا جاوا برای سمت Client ) استفاده خواهیم کرد.
خب ابتدا بهتر است که سمت سرور با Node js را پیش ببریم. فایل package.json که تمامی وابستگی های node js را مدیریت می کند و index.js که سرور اصلی ما ست. پس از ساخت این دو فایل، command line پایین پروژه را باز کنید و دستور زیر را اجرا کنید:
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')
});
برای اینکه از اجرای سرور خود اطمینان حاصل کنید به command line رفته و دستور زیر را اجرا کنید:
node index.js
نکته :
درصوتی که از دستور node استفاده کنید می توانید هر سروری که با node ساخته شده را اجرا کنید اما مشکلی که این روش دارد این است که باید همان دستور را هر زمان که فایل index.js را تغییر دادیم، اجرا کنیم. پس برای ساده تر کردن کار بهتر است از دستور nodemon استفاده کنید زیرا nodemon هرزمان که تغییری ایجاد کنید سرور را ری استارت می کند.
پس برای نصب nodemon درون command line دستور زیر را اجرا کنید:
npm install -g nodemon
اگر سرور شما در حال اجرا باشد باید دستور زیر را در log کنسول ببینید.
و حالا قسمت جالب کار است که باید به سراغ سوکت نویسی برویم ! قبل از شروع این قسمت باید به این نکته توجه داشته باشیم که Socket.io تماما برروی event ها سوار است و تمامی فعالیت خود را با event مدیریت می کند.
اما event چیست؟
می توان اینگونه گفت که event ها کلید های قرار دادی بین server و client برای اطلاع از وضعیت و درخواست یکدیگرند.
یکی event هایی که توسط خود Socket.io پیاده و مدیریت می شوند مانند event های connection و disconnect که زمان اتصال و قطع اتصال client به سرور توسط خود Socket.io فراخوانی می شود و یا message که event ی برای ارسال و دریافت پیام است.
نوع event دوم event ای است که خود ما می توانیم بسازیم.سوکت این امکان را به ما می دهد که علاوه بر استفاده از event های خودش، به هر تعداد و هر نامی event ساخته و بین server و client به صورت قراردادی پیاده کنیم.
خب حالا زمان آن است که چند متد socket.io را در پروژه ساخت چت با اندروید برای مدیریت event های اپلیکیشن چت پیاده کنیم، که شامل event های کانکت شدن کاربر و message می باشد.در فایل index.js اولین event را به صورت زیر پیاده می کنیم که برای تشخیص اتصال کاربر به سرور می باشد:
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 گوش دادن و فراخوانی event هاست. کد فوق نشان می دهد که ما متد on مربوط به socket.io را پیاده سازی کردیم که شامل دو پارامترeventname، که یک نام قراردادی بین سرور و کلاینت است برای فراخوانی(سمت کلاینت) و گوش دادن (سمت سرور) جهت انجام دستورات مربوطه. و callcback که مشخص می کند زمان call شدن این event چه دستوراتی اجرا شود.
پس حال میتوان متد فوق را بدین صورت تفسیر کرد :
متد on برای مشخص کردن event ی است که قرار است سمت کلاینت فراخوانی شود.نام event ی که مشخص کردیم connection است برای زمانی که یک کلاینت به سرور متصل می شود.پس می توان گفت event ی با نام connection از سمت کلاینت فراخوانی می شود و سپس دستورات درون کالبک (socket) => اجرا می شود. درون دستورات کالبک ما ابتدا یک لاگ node js پیاده کردیم:
console.log('user connected')
و سپس event دیگری با نام join را جهت call شدن از سمت کلاینت پیاده کردیم:
socket.on('join', function(userNickname)
که البته این event یک پارامتر از سمت کلاینت دریافت می کند ( که اینجا با کلید userNickname آنرا دریافت می کند ). درون ایونت join مجددا یک لاگ ساده داریم که مقدار دریافتی از سمت کلاینت را لاگ می کند:
console.log(userNickname +" : has joined the chat " )
و یک متد emit از socket.io که برخلاف متد on ( که برای گوش دادن به فراخوان های از سمت کلاینت بود ) برای call یا فراخوانی کردن یک event خاص سمت client است.
socket.broadcast.emit('userjoinedthechat',userNickname +" : has joined the chat ")
همانطور که می بینید event ی که فراخوانی می شود با نام userjoinchat (که باید همین نام دقیقا سمت کلاینت جهت call شدن پیاده شود) یک مقدار هم به کلاینت ارسال میکند.
userNickname +” : has joined the chat “
که سمت کلاینت کاربر می تواند این مقدار را به راحتی دریافت و نمایش دهد.
نکته:
یک نکته درباره پارامتر هایی که در event ها ارسال یا دریافت می شود این است که این پارامتر ها کلید یا نام مشخصی نخواهند داشت بلکه تنها نکته ای که باید جهت تشخیص مقدار ها از آن استفاده کرد، ترتیب آنهاست که این هم به صورت قراردادی سمت کلاینت و سرور مشخص می شود که مثلا پارامتر اول نام کاربر و پارامتر دوم شماره موبایل کاربر خواهد بود، حال این ترتیب هم باید زمان ارسال ( از سمت کلاینت یا سرور ) و هم زمان دریافت ( در سمت کلاینت یا سرور ) رعایت شود تا دیتای درست رد و بدل شود.
نکته:
متد emit ایونتی که مشخص کردید را به تمامی کاربرانی که به سرور join هستند ارسال می کند.و اما اگر قصد داشتیم که ایونت را برای همه کاربران متصل به سرور حتی خود ارسال کننده بفرستید، کافی است از متد io.emit() بجای socket.emit() استفاده کنیم.
در ادامه آموزش ساخت چت realtime با اندروید ، NodeJs و Socket.io ما برای مدیریت پیامی که از سمت کلاینت ارسال می شود دستورات زیر را پیاده می کنیم:
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 )
});
توجه کنید که در دستورات فوق ما دو آرگومان داریم که شامل نام ارسال کننده و پیام با نام های senderNickname و messageContent است.که درواقع این مقادیر از سمت کلاینت به سرور زمان اجرای ایونت messagedetection ارسال می شود.
همانطور که در بالا نیز توضیح دادیم در ارسال و دریافت پارامتر ها در socket از هیچ کلیدی استفاده نمی شود و فقط ترتیب آرگومان ها مشخص کننده مقادیر خواهد بود و در نهایت زمانی که کاربر disconnect میشود ایونت disconnect اجرا یا fire می شود که به صورت زیر این event را هم مدیریت کرده ایم:
socket.on('disconnect', function() {
console.log( 'user has left ')
socket.broadcast.emit( "userdisconnect" ,' user has left')
});
زمان اجرای ایونت disconnect ابتدا یک لاگ انجام می شود و سپس یک event به صورت broadcast یا همگانی با نام userdisconnect به سمت تمامی کلاینت ها منتشر می شود.و در نهایت فایل 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')
})
حال می توانیم به بخش کلاینت ( Client ) پروژه یعنی پلتفرم اندروید بپردازیم، پیش از هر کدنویسی باید کتابخانه سوکت را به پروژه اندروید اضافه کنیم:
implementation('com.github.nkzawa:socket.io-client:0.5.0') {
exclude group: 'org.json', module: 'json'
}
و درنهایت پروژه را sync کنید. بهتر است که همین الان کتابخانه recyclerview هم به گریدل پروژه اضافه کنید چرا که برای نمایش پیام های چت به recyclerview نیاز خواهیم داشت:
implementation 'androidx.recyclerview:recyclerview:1.1.0'
همانطور که می بینید زمان اضافه کردن کتابخانه سوکت ما تعدادی ماژول را exclude یا حذف کردیم که زمان دانلود کتابخانه به پروژه ما اضافه نشوند چرا که نیازی به آنها نخواهیم داشت.پس از sync موفق گریدل فراموش نکنید که مجوز دسترسی به اینترنت را در manifest پروژه تعریف کنید:
<uses-permission android:name="android.permission.INTERNET" />
حال فایل activity_main.xml بدین صورت طراحی کنید:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".MainActivity">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="24dp"
app:layout_constraintBottom_toTopOf="@+id/enterchat"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/nickname"
android:hint="لطفا نام خود را وارد کنید" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/enterchat"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="32dp"
android:text="ورود به چت"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
نکته :
برای پیاده سازی TextInputLayout نیز باید کتابخانه زیر را به گریدل پروژه خود اضافه کنید.
implementation 'com.google.android.material:material:1.1.0'
در نهایت صفحه ای که طراحی کردید به صورت زیر خواهد بود:
اکنون می توان کلاس MainActivity.kt را به صورت زیر کدنویسی کرد:
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn.setOnClickListener {
if (nickname.text.toString().trim().isNotEmpty()) {
val i = Intent(this@MainActivity, ChatBoxActivity::class.java)
i.putExtra("usernickname", nickname.text.toString().trim())
startActivity(i)
}
}
}
}
تنها کاری که در کلاس MainActivity.kt انجام دادیم این است که برای دکمه ای که درون لایه بود یک شنونده کلیک یا ClickListener تعریف کردیم که به کلاس دیگری ( ChatBoxActivity.kt ) که در ادامه توضیح خواهیم داد هدایت می کند، البته به همراه نامی که کاربر درون Edittext وارد کرده.
حال زمان آن است که اکتیویتی ChatBoxActivity را ایجاد و کدنویسی کنیم. برای این کار یک اکتیویتی به همراه layout ایجاد کنید و درون لایه آن که طبیعتا با نام activity_chat_box.xml به صورت اتوماتیک ایجاد می شود. طرح زیر را با دستورات زیر پیاد کنید:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".ChatBoxActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/edt_messagelist"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/btn_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/edt_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:ems="10"
android:inputType="textPersonName"
android:hint="پیام بنویسید"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/btn_send" />
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:text="ارسال"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/edt_message"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
اما پیام هایی که درون recyclerview قرار است نمایش داده شود ( چه پیام های ارسالی و چه پیام های دریافتی ) قطعا باید در یک مدل ( model ) قرار بگیرند که این مدل بصورت زیر خواهد بود:
class Message {
var nickname: String? = null
var message: String? = null
constructor() {}
constructor(nickname: String?, message: String?) {
this.nickname = nickname
this.message = message
}
}
و حالا نیاز به یک layout داریم که درواقع آیتم پیام های ارسالی و دریافتی ما در لیست پیام ها خواهد بود:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content">
<TextView
android:id="@+id/nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:gravity="right"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/message"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:gravity="right"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/nickname"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
حال که لایه آیتم های لیست را داریم می توانیم adapter لیست را برای مدیریت پیام های درون لیست به صورت زیر پیاده کنیم:
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item.view.*
class ChatBoxAdapter(var messageList: ArrayList<Message>) :
RecyclerView.Adapter<ChatBoxAdapter.MyViewHolder>() {
inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
}
public fun addItems(message: Message){
messageList.add(message)
notifyDataSetChanged()
}
override fun getItemCount(): Int = messageList.size
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyViewHolder {
val itemView: View = LayoutInflater.from(parent.context)
.inflate(R.layout.item, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(
holder: MyViewHolder,
position: Int
) {
val m = messageList[position]
holder.itemView.nickname.text = m.nickname
holder.itemView.message.text = m.message
}
}
همانطور که مشخص است در کلاس adapter که درواقع برای مدیریت recyclerView مربوط به آیتم پیام ها خواهد بود، ما یک لیست از مدل Message که پیش تر ایجاد کردیم به عنوان پارامتر ورودی دریافت کردیم و مقادیر nickname و message هر آیتم را به textview های درون لایه item.xml اختصاص داده ایم ( درون متد onBinCiewHolder ).
نکته :
زبان برنامه نویسی کاتلین خود این دسترسی به المان های درون لایه را برای شما راحتتر می کند اما با استفاده از DataBinding و یا ButterKnife هم می توان این راحتی دسترسی به المان های درون لایه را در زبان برنامه نویسی جاوا یا حتی خود کاتلین به وجود آورد.
همچنین بخوانید:
آموزش دیتا بایندینگ در اندروید
همه چیز درباره کتابخانه ButterKnife در اندروید
خب اکنون می توان گفت تمامی پیش نیاز هایی که برای پیاده سازی اکتیویتی ChatBoxActivity.kt نیاز داریم را آماده کرده ایم. پس هم اکنون می توان درون اکتیوتی ChatBoxActivity.kt وارد مرحله جذاب سوکت نویسی در اندروید شویم و چت realtime با اندروید را به صورت کامل پیاده سازی کنیم:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.github.nkzawa.socketio.client.IO
import com.github.nkzawa.socketio.client.Socket
import java.net.URISyntaxException
class ChatBoxActivity : AppCompatActivity() {
private lateinit var socket:Socket
private var nickname = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_box)
nickname = intent.getStringExtra("usernickname") ?: "unknown"
// config socket client
try {
// ini socket variable
socket = IO.socket("http://yourlocalIpaddress:3000")
// try connect to server uri
socket.connect()
// emit an event to server
socket.emit("join" , nickname)
}catch (e: URISyntaxException){
e.printStackTrace()
}
}
}
اما دستوراتی که نوشته ایم چه عملکردی خواهند داشت. خب از اولین variable ی که ایجاد کردیم شروع می کنیم که سوکت خواهد بود:
private lateinit var socket:Socket
درواقع متغیر socket حاوی تمامی اتصالات کلاینت و سرور خواهد بود که می تواند هر نوع event ی به سمت سرور emit یا منتشر کند و هر نوع event ی از سمت سرور نیز دریافت و مدیریت کند.منظور از هرنوع event درواقع event هایی ست که در برنامه نویسی سوکت برای ارتباط بین سرور و کلاینت ایجاد می شوند. و واژه هرنوع به این خاطر به کار رفته که این event ها می توانند به هرتعدادی پارامترهای ارسالی و دریافتی داشته باشند که در هر دو سمت کلاینت و سرور به صورت قراردادی مدیریت می شود. سپس متغیر دیگری با نام nickname تعریف کردیم که درواقع محتوی آن همان مقداری است که از اکتیوتی قبل یعنی MainActivity.kt به اکتیوتی جاری یعنی ChatBoxActivity.kt ارسال شده است.
nickname = intent.getStringExtra("usernickname") ?: "unknown"
در ادامه درون متد onCreate یک try / catch خواهیم داشت که وظیفه پیاده سازی ارتباط بین سوکت کلاینت و سرور را دارد و البته به همراه منتشر کردن یک eventخاص. اولین خط درون بلاک try درواقع متغیر socket را با آدرس سروری که باید کلاینت به آن متصل شود، مقدار دهی کرده است.
socket = IO.socket("http://yourlocalIpaddress:3000")
در خط دوم متد connect مربوط به کلاس Socket را فراخوانی کرده ایم. متد connect وظیفه ارسال درخواست اتصال به آدرس سروری را می دهد که ما زمان مقدار دهی متغیر سوکت به آن داده ایم.
socket.connect()
اگر حالا شبیه ساز یا emulator خودرا راه اندازی کرده و پروژه چت realtime با اندروید را اجرا کنید ابتدا صفحه اول را می بینید که باید یک nickname وارد کنید و و با کلیک برروی دکمه به صفحه بعد بروید. سپس لاگ سرور را خواهید دید که مطابق زیر نشان می دهد چه کاربری به چت وارد شده است.به عنوان مثال:
Node app is running an port 300
user connected
iman : has joined the chat
که مشخص کننده آن است که کاربری با نام iman با موفقیت به چت join شده ( چرا که ایونت join از سمت کلاینت emit یا منتشر شده).
تابه اینجای آموزش ساخت چت realtime با اندروید ، NodeJs و Socket.io ما بیس کار و به نوعی هسته کار را انجام دادیم و یاد گرفتیم. پس از این قسمت به بعد تنها قصد داریم پروژه را بزرگتر و کارامد تر کنیم.
فراموش نکنید که سرور به صورت پیش فرض event هارا به صورت همگانی broadcast می کند. پس باید یه handler هم سمت کلاینت ( اندروید ) برای مدیریت event هایی که از سمت سرور به اصطلاح fire می شوند، پیاده کنیم تا زمان منتشر شدن event خاصی از سمت server بتوانیم دستورات مورد نظر خودرا سمت کلاینت اجرا کنیم. برای مثال زمانی که یک کاربر به چت join میشد ما سمت سرور یک event با نام userjoinedthechat منتشر می کنیم که به صورت broadcast به همه کاربران متصل به سوکت ارسال می شود و همگی آنها دریافت کننده این event خواهند بود پس باید یک handler برای این event درون ChatBoxActivity.kt به صورت زیر پیاده کنیم:
socket.on("userjoinedthechat", Emitter.Listener {
runOnUiThread {
val data = it[0]
Toast.makeText(this@ChatBoxActivity, data.toString(), Toast.LENGTH_SHORT).show()
}
})
نکته :
همانگونه که مشاهده می کنید ما از بلاک runOnUiThread استفاده کردیم چون بلاک درون سوکت ممکن است زمانی اجرا شود که برنامه روی UI thread نباشد مثلا روی Background thread باشد که در این صورت اپلیکیشن کرش خواهد کرد چرا که دستورات ما نیاز دارد که برروی UI thread اجرا شود درحالی که اپلیکیشن در آن زمان دسترسی به UI thread ندارد پس با کرشی مشابه Updating Views from non-UI threads مواجه خواهید شد. پس این دستور کلیدی را زمان اجرای دستوراتی که نیاز به UI thread دارند فراموش نکنید.
حال زمانی که پروژه را همزمان برروی بیش از یک emulator اجرا کنید خواهید دید با join شدن هر دیوایس یک Toast نمایش داده می شود که محتوی آن از سرور و با ایونت userjoinedthechat دریافت شده و نمایش داده می شود.
خب اکنون می توانیم به بخش اصلی آموزش پروژه محور چت با سوکت یعنی ارسال و دریافت پیام توسط کاربر، بپردازیم.
قطعا اولین قدم این است که یک کالبک کلیک برای دکمه ارسال پیام ایجاد کنیم که زمان تاچ این دکمه متن نوشته شده درون EditText با event مشخصی ( که در ادامه پیاده خواهیم کرد ) به سمت سرور ارسال شود و درادامه سرور هم مقدار دریافتی را برای همه کاربران broadcast کند.
در کد زیر ما ClickListener را برای دکمه ارسال پیاده کردیم و درون بلاک آن ایونت messagedetection را ( که در ابتدای آموزش سمت سرور پیاده کردیم ) به همراه دو پارامتر nickname و متن درون edt_message ( که این پارامتر هارا هم برای ایونت messagedetection در ابتدای آموزش تعریف کردیم ) با استفاده از متد emit سوکت به سمت سرور ارسال کردیم.
btn_send.setOnClickListener {
socket.emit("messagedetection", nickname, edt_message.text.toString().trim())
edt_message.setText("")
}
همانطور که گفتیم پس از انتشار ایونت messagedetection از کلاینت به سرور یک ایونت دیگر با نام message از سمت سرور منتشر یا emit میشود. برای یادآوری مجدد:
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 )
})
پس هم اکنون ما باید ایونت message را سمت کلاینت مدیریت کنیم چرا که زمان ارسال پیام توسط هر کاربر این event سمت سرور برای تمامی کلاینت ها broadcast میشود.اما پیش از آن باید RecyclerView مربوط به پیام ها را با ChatBoxAdapter به صورت زیر کانفیگ کنیم:
lateinit var chatAdapter : ChatBoxAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_box)
chatAdapter = ChatBoxAdapter(ArrayList<Message>())
rcv_messages.layoutManager = LinearLayoutManager(this)
rcv_messages.adapter = chatAdapter
...
}
و حال ایونت message را به این صورت پیاده سازی می کنیم:
socket.on("message" , Emitter.Listener {
runOnUiThread {
val data = it[0] as JSONObject
try {
val nickName = data.getString("senderNickname")
val message = data.getString("message")
val mObj = Message(nickName , message)
chatAdapter.addItems(mObj)
}catch (e: JSONException){
e.printStackTrace()
}
}
})
همانگونه که می بینید در بلاک ایونت message هم ما از runOnUiThread استفاده کردیم چون دستورات ما برروی UI باید اجرا شوند. در دوخط اول که ما دو مقدار senderNickname و message که توقع داریم از سمت سرور همراه با ایونت message ارسال شده باشد، را در قالب JsonObject دریافت می کنیم.
سپس یک Object از کلاس Message با مقادیری که دریافت کردیم می سازیم و به متد addItems مربوط به متغیر ChatBoxAdapter که در بالا ساختیم جهت اضافه شدن به لیست پیام ها پاس میدهیم. حال برنامه را روی حداقل دو شبیه ساز اجرا کنید و پیام ارسال کنید و خواهید دید پیام ها درون هردو شبیه ساز یا emulator به صورت زیر نمایش داده می شود چرا که event ها به درستی فراخوانی می شوند:
هم اکنون می توان گفت که تقریبا آموزش تمام شده و اپلیکیشن قابل اجراست اما پیش از اتمام بهتر است که متغیر socket را درون متد onDestroy بطور کامل disconnect کنید. توجه کنید که با فراخوانی متد disconnect() ایونت “disconnect” که تعریف شده درون کلاس اصلی Socket سمت کلاینت است به سمت سرور fire می شود و همانطور که در ابتدای آموزش ایونت disconnect را سمت سرور به صورت زیر مدیریت کردیم:
socket.on('disconnect', function() {
console.log(userNickname +' has left ')
socket.broadcast.emit( "userdisconnect" ,' user has left')
})
})
گفته شده که با فراخوانی ایونت disconnect یک event دیگر با نام userdisconnect به سمت تمامی کلاینت ها broadcast شود. پس ما آخرین ایونت را که userdisconnect است هم سمت بدین صورت سمت اندروید پیاده می کنیم:
socket.on("userdisconnect", Emitter.Listener {
runOnUiThread {
val data = it[0]
Toast.makeText(this@ChatBoxActivity, data.toString(), Toast.LENGTH_SHORT).show()
}
})
در انتها کلاس ChatBoxActivity.kt به صورت زیر خواهد بود:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.nkzawa.emitter.Emitter
import com.github.nkzawa.socketio.client.IO
import com.github.nkzawa.socketio.client.Socket
import kotlinx.android.synthetic.main.activity_chat_box.*
import org.json.JSONException
import org.json.JSONObject
import java.net.URISyntaxException
class ChatBoxActivity : AppCompatActivity() {
private lateinit var socket: Socket
private var nickname = ""
lateinit var chatAdapter : ChatBoxAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_box)
chatAdapter = ChatBoxAdapter(ArrayList<Message>())
rcv_messages.layoutManager = LinearLayoutManager(this)
rcv_messages.adapter = chatAdapter
nickname = intent.getStringExtra("usernickname") ?: "unknown"
// config socket client
try {
// ini socket variable
socket = IO.socket("http://yourlocalIpaddress:3000")
// try connect to server uri
socket.connect()
// emit an event to server
socket.emit("join", nickname)
} catch (e: URISyntaxException) {
e.printStackTrace()
}
socket.on("userjoinedthechat", Emitter.Listener {
runOnUiThread {
val data = it[0]
Toast.makeText(this@ChatBoxActivity, data.toString(), Toast.LENGTH_SHORT).show()
}
})
btn_send.setOnClickListener {
socket.emit("messagedetection", nickname, edt_message.text.toString().trim())
edt_message.setText("")
}
socket.on("message" , Emitter.Listener {
runOnUiThread {
val data = it[0] as JSONObject
try {
val nickName = data.getString("senderNickname")
val message = data.getString("message")
val mObj = Message(nickName , message)
chatAdapter.addItems(mObj)
}catch (e: JSONException){
e.printStackTrace()
}
}
})
socket.on("userdisconnect", Emitter.Listener {
runOnUiThread {
val data = it[0]
Toast.makeText(this@ChatBoxActivity, data.toString(), Toast.LENGTH_SHORT).show()
}
})
}
}
امیدوارم آموزش ساخت چت realtime با اندروید ، NodeJs و Socket.io برای شما مفید بوده باشد…