[TOC]
GoのWebSocketベースの通信チャットソフトウェアを使用します。
コードリポジトリGOのCoroutinesは非常に軽量です。各クライアントにアクセスすると、各クライアントに対してコルーチンが有効になり、スタンドアロンマシンでより大きな並行性を実現できます。同時に、Go Channelはクライアントのアクセスとメッセージ転送およびその他の操作を完全に切り離すことができます。
Go-chatを使用して、チャンネルと選択の使用、ORMフレームワークの使用、Webフレームワークジンの使用、構成管理、ログ操作、および一部のプロジェクトで一般的に使用されるその他のテクノロジーをマスターできます。
Reactに基づいて、UIと基本コンポーネントはANT設計で使用されます。フロントエンドインターフェイスを構築するのは非常に便利です。
インターフェイスで単一ページフレームを選択すると、チャットインターフェイスを作成する方が便利です。たとえば、メッセージのリマインダーなど、リマインダーのために1つのインターフェイスでメッセージを受信できます。メッセージの受け入れは、ページを変更したり、他のコンテンツを表示したりすることで影響を受けません。フロントエンドコードリポジトリ: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.goNodejsなどのReact Basic環境を構成します...
コードをプルします
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.gomessage.protoが変更されている場合、対応するGOファイルを再コンパイルして生成する必要があります。ルートディレクトリで実行します
make proto
实际执行命令是Makefile下的
protoc --gogo_out=. protocol/ * .protoProtoファイルがローカルにインストールされていない場合は、最初にインストールする必要があります。そうしないと、Protocコマンドが見つかりません。 Gogoprotobufを使用します
Protobufライブラリファイルをインストールします
go get github.com/golang/protobuf/protoProtoc-Gen-Gogoをインストールします
go get github.com/gogo/protobuf/protoc-gen-gogoGogoprotobufライブラリファイルをインストールします
go get github.com/gogo/protobuf/protoルートディレクトリでのテスト:
protoc --gogo_out=. protocol/ * .protoフロントエンドは、Protocバッファーライブラリをインストールする必要があります
npm install protobufjsProtocのJSファイルをディレクトリに生成します
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 是自己写的字段等このファイルはジンのルーティングマップであり、通常のゲットリクエストを行い、ソケット接続にアップグレードします
// 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 ()
}これらは、サーバーの3つのチャネルです。
// 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プロトコルまたはローカルホスト、127.0.0.1ローカルIPアドレスでなければなりません。すべてのローカルテストでは、複数のブラウザを開くか、これら2つのローカルIPを使用してそれぞれ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 = [ ]
}