[TOC]
استخدم برنامج دردشة الاتصالات المستندة إلى WebSocket.
coroutines في مستودع الكود GO خفيفة الوزن للغاية. عند الوصول إلى كل عميل ، يتم تمكين coroutine لكل عميل ، والذي يمكن أن يحقق المزيد من التزامن على جهاز مستقل. في الوقت نفسه ، يمكن لـ GO Cannel رفض وصول العميل بشكل مثالي وإعادة توجيه الرسائل وغيرها من العمليات.
من خلال GO-Chat ، يمكنك إتقان استخدام القناة واختياره ، واستخدام ORM Framework ، واستخدام GIN Framework Gin ، وإدارة التكوين ، وتشغيل السجل ، وغيرها من التقنيات الشائعة الاستخدام في بعض المشاريع.
بناءً على React ، يتم استخدام واجهة المستخدم والمكونات الأساسية مع تصميم ANT. يمكن أن يكون مريحًا جدًا لبناء الواجهة الأمامية.
إن تحديد إطار صفحة واحدة على الواجهة يجعله أكثر ملاءمة لكتابة واجهة الدردشة. على سبيل المثال ، على سبيل المثال ، تذكير الرسائل ، يمكنك تلقي الرسائل في واجهة واحدة للتذكير ، ولن يتأثر قبول الرسائل بتغيير الصفحات أو عرض المحتوى الآخر. مستودع الرمز الأمامي: https://github.com/kone-net/go-chat-web
صوت ، نص ، صور ، رسائل فيديو 
مكالمة فيديو 
مشاركة الشاشة 
syntax = "proto3" ;
package protocol;
message Message {
string avatar = 1 ; //头像
string fromUsername = 2 ; // 发送消息用户的用户名
string from = 3 ; // 发送消息用户uuid
string to = 4 ; // 发送给对端用户的uuid
string content = 5 ; // 文本消息内容
int32 contentType = 6 ; // 消息内容类型:1.文字 2.普通文件 3.图片 4.音频 5.视频 6.语音聊天 7.视频聊天
string type = 7 ; // 如果是心跳消息,该内容为heatbeat
int32 messageType = 8 ; // 消息类型,1.单聊 2.群聊
string url = 9 ; // 图片,视频,语音的路径
string fileSuffix = 10 ; // 文件后缀,如果通过二进制头不能解析文件后缀,使用该后缀
bytes file = 11 ; // 如果是图片,文件,视频等的二进制
}من جسم الرسائل ، يمكننا أن نرى أن معظم الرسائل عبارة عن سلاسل أو أنواع عدد صحيح. يمكن إجراء الإرسال من خلال JSON. ثم لماذا تختار عازلة بروتوكول Google للإرسال؟
التكوين الأساسي لبيئة GO ...
سحب رمز الظهر
git clone https://github.com/kone-net/go-chatاذهب إلى الدليل
cd go-chatالتبعيات المطلوبة لسحب البرنامج
go mod downloadMySQL ينشئ قاعدة بيانات
CREATE DATABASE chat ;تعديل ملف تكوين قاعدة البيانات
vim config.toml
[mysql]
host = " 127.0.0.1 "
name = " chat "
password = " root1234 "
port = 3306
table_prefix = " "
user = " root "
修改用户名user,密码password等信息。إنشاء جدول
将chat.sql里面的sql语句复制到控制台创建对应的表。أضف مستخدمًا تهيئًا في جدول المستخدم
手动添加用户。تشغيل البرنامج
go run cmd/main.goتكوين بيئة React Basic ، مثل Nodejs ...
اسحب الكود
git clone https://github.com/kone-net/go-chat-webاذهب إلى الدليل
cd go-chat-webالتبعيات الأساسية للتركيب الواجهة الأمامية
npm installإذا كان من الضروري تعديل عنوان الواجهة الخلفية أو رقم المنفذ ، فيجب تعديل عنوان الواجهة الخلفية عند تشغيل الخادم.
修改src/chat/common/param/Params.jsx里面的IP_PORTمنفذ بدء التشغيل الافتراضي لتشغيل الرمز الأمامي هو 3000
npm startالوصول إلى البوابة الأمامية
http://127.0.0.1:3000/login
docker build -t konenet/gochat:1.0 .
appName = " chat_room "
[ mysql ]
host = " mysql8 "
name = " go-chat-message "
password = " thepswdforroot "
port = 3306
tablePrefix = " "
user = " root "
[ log ]
level = " debug "
path = " logs/chat.log "
[ staticPath ]
filePath = " web/static/file/ "
[ msgChannelType ]
channelType = " kafka "
kafkaHosts = " kafka:9092 "
kafkaTopic = " go-chat-message " docker-compose up -d
├── Makefile 代码编译,打包,结构化等操作
├── README.md
├── api controller类,对外的接口,如添加好友,查找好友等。所有http请求的入口
│ └── v1
├── assets
│ └── screenshot 系统使用到的资源,markdown用到的截图文件
├── bin 打包的二进制文件
├── chat.sql 整个项目的SQL
├── cmd
│ └── main.go main函数入口,程序启动
├── config
│ └── toml_config.go 系统全局的配置文件配置类
├── config.toml 配置文件
├── deployments
│ └── docker docker构建镜像,docker-compose.yml等文件
├── go.mod
├── go.sum
├── internal
│ ├── dao 数据库
│ ├── kafka kafka消费者和生产者
│ ├── model 数据库模型,和表一一对应
│ ├── router gin和controller类进行绑定
│ ├── server WebSocket中消息的接受和转发的主要逻辑
│ └── service 调用的服务类
├── logs
├── pkg
│ ├── common 常量,工具类
│ ├── errors 封装的异常类
│ ├── global 封装的日志类,使用时不会出现第三方的包依赖
│ └── protocol protoc buffer自动生成的文件,定义的protoc buffer字段
├── test
│ └── kafka_test.go
└── web
└── static 上传的文件等
قم بتنفيذ الأمر Make Mac في دليل الجذر
make build-darwin
实际执行命令是Makefile下的
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/chat cmd/main.goLinux
make build
实际执行命令是Makefile下的
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/chat cmd/main.goإذا تم تعديل message.proto ، فأنت بحاجة إلى إعادة ترجمة ملف GO المقابل وإنشاءه. تنفيذ في دليل الجذر
make proto
实际执行命令是Makefile下的
protoc --gogo_out=. protocol/ * .protoإذا لم يتم تثبيت ملف proto محليًا ، فأنت بحاجة إلى تثبيته أولاً ، وإلا فلن يتم العثور على أمر protoc. باستخدام Gogoprotobuf
قم بتثبيت ملف مكتبة Protobuf
go get github.com/golang/protobuf/protoتثبيت protoc-gen-gogo
go get github.com/gogo/protobuf/protoc-gen-gogoتثبيت ملف مكتبة Gogoprotobuf
go get github.com/gogo/protobuf/protoاختبار في دليل الجذر:
protoc --gogo_out=. protocol/ * .protoيحتاج الواجهة الأمامية إلى تثبيت مكتبة Protoc Buffer
npm install protobufjsقم بإنشاء ملف JS الخاص بـ Protoc إلى الدليل
npx pbjs -t json-module -w commonjs -o src/chat/proto/proto.js src/chat/proto/ * .proto
src/chat/proto/proto.js 是生成的文件的目录路径及其文件名称
src/chat/proto/ * .proto 是自己写的字段等هذا الملف عبارة عن خريطة توجيه لـ Gin ، والتي ستجعل طلبات Get العادية والترقية إلى Socket Connection
// router/router.go
func NewRouter () * gin. Engine {
gin . SetMode ( gin . ReleaseMode )
server := gin . Default ()
server . Use ( Cors ())
server . Use ( Recovery )
socket := RunSocekt
group := server . Group ( "" )
{
...
group . GET ( "/socket.io" , socket )
}
return server
}يقوم هذا الجزء بترقية الطلب إلى WebSocket.
// router/socket.go
var upGrader = websocket. Upgrader {
CheckOrigin : func ( r * http. Request ) bool {
return true
},
}
func RunSocekt ( c * gin. Context ) {
user := c . Query ( "user" )
if user == "" {
return
}
log . Info ( "newUser" , zap . String ( "newUser" , user ))
ws , err := upGrader . Upgrade ( c . Writer , c . Request , nil ) //升级协议为WebSocket
if err != nil {
return
}
client := & server. Client {
Name : user ,
Conn : ws ,
Send : make ( chan [] byte ),
}
server . MyServer . Register <- client
go client . Read ()
go client . Write ()
}هذه هي القنوات الثلاث للخادم.
// server/server.go
func ( s * Server ) Start () {
log . Info ( "start server" , log . Any ( "start server" , "start server..." ))
for {
select {
case conn := <- s . Register :
log . Info ( "login" , log . Any ( "login" , "new user login in" + conn . Name ))
s . Clients [ conn . Name ] = conn
msg := & protocol. Message {
From : "System" ,
To : conn . Name ,
Content : "welcome!" ,
}
protoMsg , _ := proto . Marshal ( msg )
conn . Send <- protoMsg
case conn := <- s . Ungister :
log . Info ( "loginout" , log . Any ( "loginout" , conn . Name ))
if _ , ok := s . Clients [ conn . Name ]; ok {
close ( conn . Send )
delete ( s . Clients , conn . Name )
}
case message := <- s . Broadcast :
msg := & protocol. Message {}
proto . Unmarshal ( message , msg )
...
...
}
}
}لتحميل ملف الحافظة ، نحتاج أولاً إلى الحصول على ملف الحافظة. كما في الكود التالي:
bindParse = ( ) => {
document . getElementById ( "messageArea" ) . addEventListener ( "paste" , ( e ) => {
var data = e . clipboardData
if ( ! data . items ) {
return ;
}
var items = data . items
if ( null == items || items . length <= 0 ) {
return ;
}
let item = items [ 0 ]
if ( item . kind !== 'file' ) {
return ;
}
let blob = item . getAsFile ( )
let reader = new FileReader ( )
reader . readAsArrayBuffer ( blob )
reader . onload = ( ( e ) => {
let imgData = e . target . result
// 上传文件必须将ArrayBuffer转换为Uint8Array
let data = {
fromUsername : localStorage . username ,
from : this . state . fromUser ,
to : this . state . toUser ,
messageType : this . state . messageType ,
content : this . state . value ,
contentType : 3 ,
file : new Uint8Array ( imgData )
}
let message = protobuf . lookup ( "protocol.Message" )
const messagePB = message . create ( data )
socket . send ( message . encode ( messagePB ) . finish ( ) )
this . appendImgToPanel ( imgData )
} )
} , false )
}نفس مبدأ تحميل الصوت
ملاحظة خاصة: للحصول على أذونات مكالمات الفيديو والصوت والشاشة ، يجب أن يكون بروتوكول HTTPS أو LocalHost ، 127.0.0.1 عنوان IP المحلي. يمكن لجميع الاختبارات المحلية فتح العديد من المتصفحات ، أو استخدام هاتين IPS المحليين لإجراء اختبارات 2TAB على التوالي.
/**
* 当按下按钮时录制视频
*/
dataChunks = [ ] ;
recorder = null ;
startVideoRecord = ( e ) => {
navigator . getUserMedia = navigator . getUserMedia ||
navigator . webkitGetUserMedia ||
navigator . mozGetUserMedia ||
navigator . msGetUserMedia ; //获取媒体对象(这里指摄像头)
let preview = document . getElementById ( "preview" ) ;
this . setState ( {
isRecord : true
} )
navigator . mediaDevices
. getUserMedia ( {
audio : true ,
video : true ,
} ) . then ( ( stream ) => {
preview . srcObject = stream ;
this . recorder = new MediaRecorder ( stream ) ;
this . recorder . ondataavailable = ( event ) => {
let data = event . data ;
this . dataChunks . push ( data ) ;
} ;
this . recorder . start ( 1000 ) ;
} ) ;
}
/**
* 松开按钮发送视频到服务器
* @param {事件} e
*/
stopVideoRecord = ( e ) => {
this . setState ( {
isRecord : false
} )
let recordedBlob = new Blob ( this . dataChunks , { type : "video/webm" } ) ;
let reader = new FileReader ( )
reader . readAsArrayBuffer ( recordedBlob )
reader . onload = ( ( e ) => {
let fileData = e . target . result
// 上传文件必须将ArrayBuffer转换为Uint8Array
let data = {
fromUsername : localStorage . username ,
from : this . state . fromUser ,
to : this . state . toUser ,
messageType : this . state . messageType ,
content : this . state . value ,
contentType : 3 ,
file : new Uint8Array ( fileData )
}
let message = protobuf . lookup ( "protocol.Message" )
const messagePB = message . create ( data )
socket . send ( message . encode ( messagePB ) . finish ( ) )
} )
this . setState ( {
comments : [
... this . state . comments ,
{
author : localStorage . username ,
avatar : this . state . user . avatar ,
content : < p > < video src = { URL . createObjectURL ( recordedBlob ) } controls autoPlay = { false } preload = "auto" width = '200px' /> </ p > ,
datetime : moment ( ) . fromNow ( ) ,
} ,
] ,
} , ( ) => {
this . scrollToBottom ( )
} )
if ( this . recorder ) {
this . recorder . stop ( )
this . recorder = null
}
let preview = document . getElementById ( "preview" ) ;
preview . srcObject . getTracks ( ) . forEach ( ( track ) => track . stop ( ) ) ;
this . dataChunks = [ ]
}