590 lines
18 KiB
Go
590 lines
18 KiB
Go
package ws
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"math/rand"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
// مقادیر پیشفرض مقاومسازی (قابل بازنویسی در تست).
|
|
const (
|
|
defaultTurnTimeout = 30 * time.Second // مهلت نوبت بازیکنِ انسان
|
|
defaultBotDelay = 800 * time.Millisecond // تأخیر حرکت بات/قطعشده (حس طبیعی + فرصت بازگشت)
|
|
defaultMatchWait = 6 * time.Second // مهلت پر شدن میز با انسان پیش از افزودن بات
|
|
defaultTrickHold = 1200 * time.Millisecond // مدت نمایش دستِ کامل پیش از جمعآوری
|
|
defaultFirstHandDelay = 3000 * time.Millisecond // تأخیر اولین حرکتِ بات تا پایانِ اینترو+بُر زدنِ کلاینت
|
|
defaultDealDelay = 1600 * time.Millisecond // تأخیر اولین بازی تا پایانِ انیمیشنِ پخشِ کارتها
|
|
)
|
|
|
|
// AuthFunc توکن را تأیید کرده و (شناسه کاربر، نام نمایشی) را برمیگرداند.
|
|
type AuthFunc func(token string) (userID int64, name string, err error)
|
|
|
|
// inbound پیام ورودی یک کلاینت برای پردازش در هاب.
|
|
type inbound struct {
|
|
client *Client
|
|
msg inboundMsg
|
|
}
|
|
|
|
// location محل بازیکن در حال بازی (برای reconnect).
|
|
type location struct {
|
|
room *Room
|
|
seat int
|
|
}
|
|
|
|
// endInfo اطلاعاتی که میز هنگام بستهشدن به هاب میدهد تا نگاشتها پاک شوند.
|
|
type endInfo struct {
|
|
room *Room
|
|
humanIDs []int64
|
|
clients []*Client
|
|
}
|
|
|
|
// pendingTable میز خصوصیِ در انتظارِ شروع (دورهمی).
|
|
// فقط توسط goroutine هاب خوانده/نوشته میشود.
|
|
type pendingTable struct {
|
|
code string
|
|
host int64
|
|
clients []*Client
|
|
hands int // تعداد دستِ انتخابشده توسط میزبان (۳/۵/۷)
|
|
rot int // چرخشِ جایگاههای غیرِمیزبان (۰..۲)
|
|
stake int // شرطِ سکهایِ هر بازیکن (۰ یعنی رایگان)
|
|
started bool // پس از فشردن «شروع» توسط میزبان
|
|
}
|
|
|
|
// nonHostSeat جایگاهِ مطلقِ j-اُمین بازیکنِ غیرِمیزبان را با چرخشِ rot برمیگرداند.
|
|
// میزبان همیشه جایگاه ۰ است؛ سه جایگاهِ دیگر {۱،۲،۳} با چرخش جابهجا میشوند.
|
|
func nonHostSeat(j, rot int) int {
|
|
seats := [3]int{1, 2, 3}
|
|
return seats[((j+rot)%3+3)%3]
|
|
}
|
|
|
|
// Hub هماهنگکننده مرکزی: اتصالها، صف matchmaking و مسیریابی پیامها.
|
|
// تمام state آن فقط توسط goroutine Run تغییر میکند (single-writer، بدون قفل).
|
|
// پس از ساخت یک میز، هاب دیگر به seatInfo دست نمیزند (مالک آن goroutine میز است).
|
|
type Hub struct {
|
|
auth AuthFunc
|
|
upgrader websocket.Upgrader
|
|
|
|
clients map[*Client]bool
|
|
queues map[string][]*Client // صف انتظار به ازای هر tier
|
|
locations map[int64]location // userID → محل بازی (برای reconnect)
|
|
tables map[string]*pendingTable // کد میز → میز خصوصیِ در انتظار
|
|
roomSeq int
|
|
botSeq int
|
|
fillPending map[string]bool
|
|
settler Settler
|
|
|
|
turnTimeout time.Duration
|
|
botDelay time.Duration
|
|
matchWait time.Duration
|
|
trickHold time.Duration
|
|
firstHandDelay time.Duration // تأخیر اولین حرکتِ بات (هماهنگ با انیمیشنِ بُر زدنِ کلاینت)
|
|
dealDelay time.Duration // تأخیر اولین بازی پس از انتخابِ حکم (انیمیشنِ پخشِ کارتها)
|
|
countdownDelay time.Duration // مهلت شمارش معکوسِ میز خصوصی پیش از شروع
|
|
|
|
register chan *Client
|
|
unregister chan *Client
|
|
inbound chan inbound
|
|
endRoom chan endInfo
|
|
fill chan string // tier برای پر کردن با بات
|
|
startTbl chan string // کد میز خصوصی برای شروع (پس از شمارش معکوس)
|
|
}
|
|
|
|
func NewHub(auth AuthFunc) *Hub {
|
|
return &Hub{
|
|
auth: auth,
|
|
upgrader: websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
// در پروداکشن مبدأ را محدود کنید؛ فعلاً برای توسعه باز است.
|
|
CheckOrigin: func(r *http.Request) bool { return true },
|
|
},
|
|
clients: make(map[*Client]bool),
|
|
queues: make(map[string][]*Client),
|
|
locations: make(map[int64]location),
|
|
tables: make(map[string]*pendingTable),
|
|
fillPending: make(map[string]bool),
|
|
settler: noopSettler{},
|
|
turnTimeout: defaultTurnTimeout,
|
|
botDelay: defaultBotDelay,
|
|
matchWait: defaultMatchWait,
|
|
trickHold: defaultTrickHold,
|
|
firstHandDelay: defaultFirstHandDelay,
|
|
dealDelay: defaultDealDelay,
|
|
countdownDelay: 3 * time.Second,
|
|
register: make(chan *Client),
|
|
unregister: make(chan *Client),
|
|
inbound: make(chan inbound, 64),
|
|
endRoom: make(chan endInfo),
|
|
fill: make(chan string, 8),
|
|
startTbl: make(chan string, 8),
|
|
}
|
|
}
|
|
|
|
// SetSettler تسویهگر اقتصادی را تنظیم میکند (پیش از Run).
|
|
func (h *Hub) SetSettler(s Settler) { h.settler = s }
|
|
|
|
const defaultTier = "beginner"
|
|
|
|
// Run حلقه اصلی هاب (در یک goroutine اجرا شود).
|
|
func (h *Hub) Run() {
|
|
for {
|
|
select {
|
|
case c := <-h.register:
|
|
h.handleRegister(c)
|
|
case c := <-h.unregister:
|
|
h.handleDisconnect(c)
|
|
case in := <-h.inbound:
|
|
h.handle(in)
|
|
case e := <-h.endRoom:
|
|
h.closeRoom(e)
|
|
case tier := <-h.fill:
|
|
h.onFill(tier)
|
|
case code := <-h.startTbl:
|
|
h.onStartTable(code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleRegister اتصال جدید را ثبت میکند؛ اگر کاربر در حال بازی بود، reconnect میشود.
|
|
func (h *Hub) handleRegister(c *Client) {
|
|
h.clients[c] = true
|
|
if loc, ok := h.locations[c.UserID]; ok {
|
|
c.room = loc.room
|
|
c.seat = loc.seat
|
|
select {
|
|
case loc.room.actions <- roomAction{kind: akReconnect, seat: loc.seat, client: c}:
|
|
default:
|
|
}
|
|
slog.Info("player reconnecting", "user", c.UserID, "room", loc.room.ID)
|
|
}
|
|
}
|
|
|
|
func (h *Hub) handle(in inbound) {
|
|
c := in.client
|
|
switch in.msg.Type {
|
|
case "join_queue":
|
|
if c.room == nil {
|
|
tier := in.msg.Tier
|
|
if tier == "" {
|
|
tier = defaultTier
|
|
}
|
|
h.enqueue(c, tier)
|
|
}
|
|
case "choose_trump", "play_card", "reshuffle", "chat", "leave":
|
|
if c.room != nil {
|
|
if in.msg.Type == "leave" {
|
|
delete(h.locations, c.UserID) // پس از خروج دائمی، reconnect نشود
|
|
}
|
|
select {
|
|
case c.room.actions <- roomAction{kind: akInput, seat: c.seat, client: c, msg: in.msg}:
|
|
default:
|
|
}
|
|
}
|
|
case "create_table":
|
|
h.createTable(c)
|
|
case "join_table":
|
|
h.joinTable(c, in.msg.Code)
|
|
case "start_table":
|
|
h.startTable(c, in.msg.Hands, in.msg.Stake)
|
|
case "set_stake":
|
|
h.setStake(c, in.msg.Stake)
|
|
case "rotate_table":
|
|
h.rotateTable(c, in.msg.Dir)
|
|
case "leave_table":
|
|
h.leaveTable(c)
|
|
}
|
|
}
|
|
|
|
// --- میز خصوصی (دورهمی) ---
|
|
|
|
// genCode یک کد ۵ رقمیِ یکتا برای میز خصوصی میسازد.
|
|
func (h *Hub) genCode() string {
|
|
for {
|
|
code := fmt.Sprintf("%05d", rand.Intn(100000))
|
|
if _, ok := h.tables[code]; !ok {
|
|
return code
|
|
}
|
|
}
|
|
}
|
|
|
|
// createTable یک میز خصوصی میسازد (در صورت مجاز بودنِ سهمیه).
|
|
func (h *Hub) createTable(c *Client) {
|
|
if c.room != nil || c.tableCode != "" {
|
|
return
|
|
}
|
|
if err := h.settler.ChargePrivateTable(c.UserID); err != nil {
|
|
c.trySend(mustJSON(errorMsg{Type: "error", Message: "سهمیهی میزهای رایگان به پایان رسیده؛ برای میز نامحدود VIP بگیرید"}))
|
|
return
|
|
}
|
|
code := h.genCode()
|
|
t := &pendingTable{code: code, host: c.UserID, clients: []*Client{c}}
|
|
h.tables[code] = t
|
|
c.tableCode = code
|
|
slog.Info("private table created", "code", code, "host", c.UserID)
|
|
h.broadcastLobby(t)
|
|
}
|
|
|
|
// joinTable کلاینت را به میز خصوصیِ موجود میافزاید.
|
|
func (h *Hub) joinTable(c *Client, code string) {
|
|
if c.room != nil || c.tableCode != "" {
|
|
return
|
|
}
|
|
t := h.tables[code]
|
|
if t == nil || t.started {
|
|
c.trySend(mustJSON(errorMsg{Type: "error", Message: "میز یافت نشد"}))
|
|
return
|
|
}
|
|
if len(t.clients) >= 4 {
|
|
c.trySend(mustJSON(errorMsg{Type: "error", Message: "میز پر است"}))
|
|
return
|
|
}
|
|
t.clients = append(t.clients, c)
|
|
c.tableCode = code
|
|
h.broadcastLobby(t)
|
|
}
|
|
|
|
// startTable شمارش معکوس را آغاز و پس از آن بازی را شروع میکند (فقط میزبان).
|
|
func (h *Hub) startTable(c *Client, hands, stake int) {
|
|
t := h.tables[c.tableCode]
|
|
if t == nil || t.host != c.UserID || t.started {
|
|
return
|
|
}
|
|
t.hands = clampHands(hands) // تعداد دستِ انتخابیِ میزبان (۳/۵/۷)
|
|
if stake >= 0 {
|
|
t.stake = stake // آخرین شرطِ سکهای (اگر همراهِ شروع فرستاده شد)
|
|
}
|
|
t.started = true
|
|
cd := mustJSON(countdownMsg{Type: "countdown", Seconds: 3})
|
|
for _, cl := range t.clients {
|
|
cl.trySend(cd)
|
|
}
|
|
code := t.code
|
|
time.AfterFunc(h.countdownDelay, func() {
|
|
select {
|
|
case h.startTbl <- code:
|
|
default:
|
|
}
|
|
})
|
|
}
|
|
|
|
// onStartTable پس از شمارش معکوس، میز را از کلاینتهای متصل میسازد و بازی را شروع میکند.
|
|
func (h *Hub) onStartTable(code string) {
|
|
t := h.tables[code]
|
|
if t == nil {
|
|
return
|
|
}
|
|
delete(h.tables, code)
|
|
|
|
var seats [4]*seatInfo
|
|
humans := 0
|
|
for i, cl := range t.clients {
|
|
if i >= 4 || !h.clients[cl] {
|
|
continue // قطعشدهها نادیده گرفته میشوند (جایشان را بات میگیرد)
|
|
}
|
|
seat := t.seatOf(i)
|
|
seats[seat] = &seatInfo{client: cl, userID: cl.UserID, name: cl.Name, connected: true}
|
|
humans++
|
|
}
|
|
if humans == 0 {
|
|
return // همه خارج شدند
|
|
}
|
|
for s := 0; s < 4; s++ {
|
|
if seats[s] == nil {
|
|
h.botSeq++
|
|
seats[s] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)}
|
|
}
|
|
}
|
|
|
|
h.roomSeq++
|
|
room := newRoom(fmt.Sprintf("p%d", h.roomSeq), seats, h)
|
|
room.tier = defaultTier // برای جایزه/ورودی بیاثر است (میز خصوصی)
|
|
room.private = true
|
|
room.handsOverride = clampHands(t.hands) // تعداد دستِ انتخابیِ میزبان
|
|
room.stake = int64(t.stake) // شرطِ سکهایِ هر بازیکن
|
|
for s := 0; s < 4; s++ {
|
|
if seats[s].isBot {
|
|
continue
|
|
}
|
|
cl := seats[s].client
|
|
cl.room = room
|
|
cl.seat = s
|
|
cl.tableCode = ""
|
|
h.locations[cl.UserID] = location{room: room, seat: s}
|
|
}
|
|
go room.run()
|
|
slog.Info("private room started", "room", room.ID, "code", code, "humans", humans)
|
|
}
|
|
|
|
// setStake شرطِ سکهایِ میز را تنظیم و برای همه پخش میکند (فقط میزبان، پیش از شروع).
|
|
func (h *Hub) setStake(c *Client, stake int) {
|
|
t := h.tables[c.tableCode]
|
|
if t == nil || t.host != c.UserID || t.started {
|
|
return
|
|
}
|
|
if stake < 0 {
|
|
stake = 0
|
|
}
|
|
if stake > 1_000_000 {
|
|
stake = 1_000_000
|
|
}
|
|
t.stake = stake
|
|
h.broadcastLobby(t)
|
|
}
|
|
|
|
// rotateTable جایگاهِ بازیکنانِ غیرِمیزبان را دورِ میز میچرخاند (فقط میزبان).
|
|
// میزبان همیشه جایگاهِ اولش را حفظ میکند؛ چرخش تیمبندی را تغییر میدهد.
|
|
func (h *Hub) rotateTable(c *Client, dir int) {
|
|
t := h.tables[c.tableCode]
|
|
if t == nil || t.host != c.UserID || t.started {
|
|
return
|
|
}
|
|
if len(t.clients) < 2 {
|
|
return // جز میزبان کسی نیست ⇒ چیزی برای چرخش نمانده
|
|
}
|
|
d := 1
|
|
if dir < 0 {
|
|
d = -1
|
|
}
|
|
t.rot = ((t.rot+d)%3 + 3) % 3
|
|
h.broadcastLobby(t)
|
|
}
|
|
|
|
// leaveTable خروجِ داوطلبانه از اتاق انتظار.
|
|
func (h *Hub) leaveTable(c *Client) {
|
|
t := h.tables[c.tableCode]
|
|
c.tableCode = ""
|
|
if t != nil {
|
|
h.removeFromTable(t, c)
|
|
}
|
|
}
|
|
|
|
// removeFromTable کلاینت را از میز حذف میکند؛ با خروجِ میزبان یا خالیشدن، میز منحل میشود.
|
|
func (h *Hub) removeFromTable(t *pendingTable, c *Client) {
|
|
idx := -1
|
|
for i, cl := range t.clients {
|
|
if cl == c {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx < 0 {
|
|
return
|
|
}
|
|
t.clients = append(t.clients[:idx], t.clients[idx+1:]...)
|
|
if c.UserID == t.host || len(t.clients) == 0 {
|
|
closed := mustJSON(tableClosedMsg{Type: "table_closed", Reason: "host_left"})
|
|
for _, cl := range t.clients {
|
|
cl.tableCode = ""
|
|
cl.trySend(closed)
|
|
}
|
|
delete(h.tables, t.code)
|
|
return
|
|
}
|
|
h.broadcastLobby(t)
|
|
}
|
|
|
|
// broadcastLobby وضعیت اتاق انتظار را برای همهی اعضای میز ارسال میکند.
|
|
func (h *Hub) broadcastLobby(t *pendingTable) {
|
|
players := make([]lobbyPlayer, len(t.clients))
|
|
for i, cl := range t.clients {
|
|
_, _, avatar := h.settler.PlayerProfile(cl.UserID)
|
|
players[i] = lobbyPlayer{
|
|
Name: cl.Name,
|
|
Avatar: avatar,
|
|
Host: cl.UserID == t.host,
|
|
Seat: t.seatOf(i),
|
|
}
|
|
}
|
|
for i, cl := range t.clients {
|
|
remaining, unlimited := h.settler.PrivateTableInfo(cl.UserID)
|
|
cl.trySend(mustJSON(tableLobbyMsg{
|
|
Type: "table_lobby", Code: t.code, Players: players,
|
|
Host: cl.UserID == t.host, YouSeat: t.seatOf(i),
|
|
Remaining: remaining, Unlimited: unlimited, Stake: t.stake,
|
|
}))
|
|
}
|
|
}
|
|
|
|
// seatOf جایگاهِ مطلقِ کلاینتِ شماره i در فهرستِ میز را برمیگرداند (میزبان=۰).
|
|
func (t *pendingTable) seatOf(i int) int {
|
|
if i == 0 {
|
|
return 0
|
|
}
|
|
return nonHostSeat(i-1, t.rot)
|
|
}
|
|
|
|
// clampHands تعداد دست را به یکی از مقادیرِ مجاز (۳/۵/۷) محدود میکند.
|
|
func clampHands(h int) int {
|
|
switch h {
|
|
case 5:
|
|
return 5
|
|
case 7:
|
|
return 7
|
|
default:
|
|
return 3
|
|
}
|
|
}
|
|
|
|
// enqueue کلاینت را پس از کسر ورودی به صفِ tier افزوده و میزهای کامل را میسازد.
|
|
func (h *Hub) enqueue(c *Client, tier string) {
|
|
if c.room != nil {
|
|
return
|
|
}
|
|
for _, q := range h.queues[tier] {
|
|
if q == c {
|
|
return
|
|
}
|
|
}
|
|
// کسر ورودی میز؛ اگر سکه کافی نبود، اجازه ورود به صف داده نمیشود.
|
|
if err := h.settler.ChargeEntry(c.UserID, tier); err != nil {
|
|
c.trySend(mustJSON(errorMsg{Type: "error", Message: "موجودی سکه برای ورود به این میز کافی نیست"}))
|
|
return
|
|
}
|
|
c.tier = tier
|
|
h.queues[tier] = append(h.queues[tier], c)
|
|
for len(h.queues[tier]) >= 4 {
|
|
h.formTable(tier, 4)
|
|
}
|
|
h.maybeArmFill(tier)
|
|
}
|
|
|
|
// onFill با سررسید مهلت، میز ناقصِ یک tier را با بات کامل میکند.
|
|
func (h *Hub) onFill(tier string) {
|
|
h.fillPending[tier] = false
|
|
n := len(h.queues[tier])
|
|
if n == 0 {
|
|
return
|
|
}
|
|
if n > 4 {
|
|
n = 4
|
|
}
|
|
h.formTable(tier, n)
|
|
h.maybeArmFill(tier)
|
|
}
|
|
|
|
// maybeArmFill در صورت وجود بازیکن در صفِ tier، تایمر پر کردن با بات را مسلح میکند.
|
|
func (h *Hub) maybeArmFill(tier string) {
|
|
if len(h.queues[tier]) > 0 && !h.fillPending[tier] {
|
|
h.fillPending[tier] = true
|
|
time.AfterFunc(h.matchWait, func() {
|
|
select {
|
|
case h.fill <- tier:
|
|
default:
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// formTable یک میزِ tier با nHumans بازیکن از ابتدای صف و بات برای بقیه میسازد.
|
|
func (h *Hub) formTable(tier string, nHumans int) {
|
|
var seats [4]*seatInfo
|
|
for i := 0; i < nHumans; i++ {
|
|
c := h.queues[tier][i]
|
|
seats[i] = &seatInfo{client: c, userID: c.UserID, name: c.Name, connected: true}
|
|
}
|
|
h.queues[tier] = h.queues[tier][nHumans:]
|
|
for i := nHumans; i < 4; i++ {
|
|
h.botSeq++
|
|
seats[i] = &seatInfo{isBot: true, name: fmt.Sprintf("ربات %d", h.botSeq)}
|
|
}
|
|
|
|
h.roomSeq++
|
|
room := newRoom(fmt.Sprintf("r%d", h.roomSeq), seats, h)
|
|
room.tier = tier
|
|
// ستکردن اشارهگرها و نگاشت reconnect پیش از شروع goroutine میز (happens-before).
|
|
for i := 0; i < nHumans; i++ {
|
|
c := seats[i].client
|
|
c.room = room
|
|
c.seat = i
|
|
h.locations[c.UserID] = location{room: room, seat: i}
|
|
}
|
|
go room.run()
|
|
slog.Info("room created", "room", room.ID, "tier", tier, "humans", nHumans, "bots", 4-nHumans)
|
|
}
|
|
|
|
// handleDisconnect قطع اتصال یک کلاینت را مدیریت میکند (نگاشت reconnect حفظ میشود).
|
|
func (h *Hub) handleDisconnect(c *Client) {
|
|
if !h.clients[c] {
|
|
return
|
|
}
|
|
delete(h.clients, c)
|
|
c.close() // writePump را خاتمه میدهد؛ send بسته نمیشود تا room بدون panic بتواند trySend کند
|
|
|
|
// اگر در صف بود، حذف و ورودی بازگردانده شود (هنوز بازی شروع نشده).
|
|
if q := h.queues[c.tier]; len(q) > 0 {
|
|
for i, x := range q {
|
|
if x == c {
|
|
h.queues[c.tier] = append(q[:i], q[i+1:]...)
|
|
h.settler.Refund(c.UserID, c.tier)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// اگر در اتاق انتظارِ میز خصوصی بود، از آن حذف شود.
|
|
if c.tableCode != "" {
|
|
if t := h.tables[c.tableCode]; t != nil {
|
|
h.removeFromTable(t, c)
|
|
}
|
|
c.tableCode = ""
|
|
}
|
|
if c.room != nil {
|
|
select {
|
|
case c.room.actions <- roomAction{kind: akDisconnect, seat: c.seat, client: c}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// closeRoom پس از پایان بازی، نگاشتها و اشارهگرهای میز را پاک میکند.
|
|
func (h *Hub) closeRoom(e endInfo) {
|
|
for _, id := range e.humanIDs {
|
|
if loc, ok := h.locations[id]; ok && loc.room == e.room {
|
|
delete(h.locations, id)
|
|
}
|
|
}
|
|
for _, c := range e.clients {
|
|
if c.room == e.room {
|
|
c.room = nil
|
|
c.seat = 0
|
|
}
|
|
}
|
|
slog.Info("room closed", "room", e.room.ID)
|
|
}
|
|
|
|
// ServeWS اتصال WebSocket را پس از تأیید توکن برقرار میکند.
|
|
// توکن از پارامتر کوئری ?token= یا هدر Authorization خوانده میشود.
|
|
func (h *Hub) ServeWS(w http.ResponseWriter, r *http.Request) {
|
|
token := r.URL.Query().Get("token")
|
|
if token == "" {
|
|
if a := r.Header.Get("Authorization"); len(a) > 7 && a[:7] == "Bearer " {
|
|
token = a[7:]
|
|
}
|
|
}
|
|
userID, name, err := h.auth(token)
|
|
if err != nil {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
conn, err := h.upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
return // upgrader خودش پاسخ خطا را نوشته
|
|
}
|
|
|
|
c := &Client{
|
|
hub: h,
|
|
conn: conn,
|
|
send: make(chan []byte, sendBuffer),
|
|
done: make(chan struct{}),
|
|
UserID: userID,
|
|
Name: name,
|
|
}
|
|
h.register <- c
|
|
go c.writePump()
|
|
go c.readPump()
|
|
}
|