[TOC]
Go의 WebSocket 기반 통신 채팅 소프트웨어를 사용하십시오.
코드 리포지토리 GO의 코 루틴은 매우 가볍습니다. 각 클라이언트에 액세스되면 각 클라이언트에 대해 코 루틴이 활성화되어 독립형 기계에서 더 큰 동시성을 달성 할 수 있습니다. 동시에 Go Channel은 클라이언트 액세스 및 메시지 전달 및 기타 작업을 완벽하게 분리 할 수 있습니다.
Go-Chat을 통해 채널 사용 및 Select, ORM 프레임 워크 사용, 웹 프레임 워크 GIN 사용, 구성 관리, 로그 운영 및 일부 프로젝트에서 일반적으로 사용되는 기술을 선택할 수 있습니다.
RECT를 기반으로 UI 및 기본 구성 요소는 개미 설계에 사용됩니다. 프론트 엔드 인터페이스를 구축하는 것이 매우 편리 할 수 있습니다.
인터페이스에서 단일 페이지 프레임을 선택하면 채팅 인터페이스를 작성하는 것이 더 편리합니다. 예를 들어, 메시지 알림, 알림에 대한 하나의 인터페이스로 메시지를받을 수 있으며, 메시지 수락은 페이지를 변경하거나 다른 컨텐츠를 보면 영향을받지 않습니다. 프론트 엔드 코드 저장소 : 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와 같은 반응 기본 환경 구성 ...
코드를 당기십시오
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 Command Mac을 실행하십시오
make build-darwin
实际执行命令是Makefile下的
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/chat cmd/main.go리눅스
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/ * .proto프로토 파일이 로컬로 설치되지 않은 경우 먼저 설치해야합니다. 그렇지 않으면 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 是自己写的字段等이 파일은 GIN의 라우팅 맵으로, 일반적인 요청 및 소켓 연결로 업그레이드합니다.
// 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 주소 여야합니다. 모든 로컬 테스트는 여러 브라우저를 열거 나이 두 로컬 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 = [ ]
}