[TOC]
Verwenden Sie die WebSocket-basierte Kommunikations-Chat-Software von Go.
Coroutinen im Code -Repository GO sind sehr leicht. Wenn auf jeden Client zugegriffen wird, wird für jeden Client eine Coroutine aktiviert, wodurch auf einer eigenständigen Maschine eine größere Parallelität erzielt werden kann. Gleichzeitig kann Go Channel den Client -Zugriff und die Nachrichtenweiterleitung und andere Vorgänge perfekt entkoppeln.
Über Go-Chat können Sie die Verwendung von Kanal beherrschen und die Verwendung von ORM Framework, die Verwendung von Web Framework-Gin, Konfigurationsverwaltung, Protokollbetrieb und andere häufig verwendete Technologien in einigen Projekten auswählen.
Basierend auf React werden UI- und Basiskomponenten mit ANT -Design verwendet. Es kann sehr bequem sein, die Front-End-Schnittstelle zu erstellen.
Wenn Sie einen einzelnen Seitenrahmen auf der Schnittstelle auswählen, wird es bequemer, eine Chat -Schnittstelle zu schreiben. Beispielsweise können Sie Nachrichten erinnert, Sie können Nachrichten in einer Schnittstelle für Erinnerungen empfangen, und die Nachrichtenakzeptanz wird nicht durch das Ändern von Seiten oder das Anzeigen anderer Inhalte beeinflusst. Front-end-Code-Repository: https://github.com/kone-net/go-chat-web
Stimme, Text, Bilder, Videomeldungen 
Videoanruf 
Bildschirmfreigabe 
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 ; // 如果是图片,文件,视频等的二进制
}Aus dem Nachrichtenkörper können wir sehen, dass die meisten Nachrichten Strings oder Ganzzahltypen sind. Die Übertragung kann über JSON durchgeführt werden. Warum dann den Protokollpuffer von Google für die Übertragung wählen?
Grundlegende Konfiguration der GO -Umgebung ...
Backend -Code ziehen
git clone https://github.com/kone-net/go-chatIn das Verzeichnis gehen
cd go-chatAbhängigkeiten, die zum Anziehen des Programms erforderlich sind
go mod downloadMySQL erstellt eine Datenbank
CREATE DATABASE chat ;Datenbankkonfigurationsdatei ändern
vim config.toml
[mysql]
host = " 127.0.0.1 "
name = " chat "
password = " root1234 "
port = 3306
table_prefix = " "
user = " root "
修改用户名user,密码password等信息。Erstellen Sie eine Tabelle
将chat.sql里面的sql语句复制到控制台创建对应的表。Fügen Sie in der Benutzertabelle den initialisierten Benutzer hinzu
手动添加用户。Führen Sie das Programm aus
go run cmd/main.goKonfigurieren Sie die React -Basic -Umgebung wie NodeJs ...
Ziehen Sie den Code
git clone https://github.com/kone-net/go-chat-webIn das Verzeichnis gehen
cd go-chat-webGrundlegende Abhängigkeiten für die Installation Front-End
npm installWenn die Backend -Adresse oder die Portnummer geändert werden muss, muss die Backend -Adresse geändert werden, wenn der Server ausgeführt wird.
修改src/chat/common/param/Params.jsx里面的IP_PORTDer Standard-Startport für das Ausführen von Front-End-Code beträgt 3000
npm startZugriff auf das Front-End-Portal
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 上传的文件等
Führen Sie den Befehl make mac im Stammverzeichnis aus
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.goWenn Message.Proto geändert wird, müssen Sie die entsprechende Go -Datei neu kompilieren und generieren. Im Stammverzeichnis ausführen
make proto
实际执行命令是Makefile下的
protoc --gogo_out=. protocol/ * .protoWenn die Proto -Datei nicht lokal installiert ist, müssen Sie sie zuerst installieren, da der Protoc -Befehl nicht gefunden wird. Verwenden von Gogoprotobuf
Installieren Sie die Protobuf -Bibliotheksdatei
go get github.com/golang/protobuf/protoInstallieren Sie Protoc-Gen-Gogo
go get github.com/gogo/protobuf/protoc-gen-gogoInstallieren Sie die Gogoprotobuf -Bibliotheksdatei
go get github.com/gogo/protobuf/protoTest im Stammverzeichnis:
protoc --gogo_out=. protocol/ * .protoDas Front-End muss die Protoc-Puffer-Bibliothek installieren
npm install protobufjsGenerieren Sie die JS -Datei von Protoc in Verzeichnis
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 是自己写的字段等Diese Datei handelt
// 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
}Dieser Teil aktualisiert die Anfrage auf 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 ()
}Dies sind die drei Kanäle des Servers.
// 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 )
...
...
}
}
}Um die Zwischenablagedatei hochzuladen, müssen wir zunächst die Zwischenablagedatei abrufen. Wie im folgenden Code:
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 )
}Das gleiche Prinzip des Hochladens der Stimme
Besonderer Hinweis: Um Video-, Audio- und Bildschirmfreigabe -Anrufberechtigungen zu erhalten, muss es HTTPS -Protokoll oder Localhost, 127.0.0.1 Lokale IP -Adresse sein. Alle lokalen Tests können mehrere Browser öffnen oder diese beiden lokalen IPs verwenden, um 2 TAB -Tests durchzuführen.
/**
* 当按下按钮时录制视频
*/
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 = [ ]
}