如何确保Golang大猩猩WebSocket包中的并发

I have studied the Godoc of the gorilla/websocket package.

In the Godoc it is clearly stated that

Concurrency Connections support one concurrent reader and one concurrent writer.

Applications are responsible for ensuring that no more than one goroutine calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and that no more than one goroutine calls the read methods (NextReader, SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler) concurrently.

The Close and WriteControl methods can be called concurrently with all other methods.

However, in one of the example provided by the package

func (c *Conn) readPump() {
    defer func() {
        hub.unregister <- c
        c.ws.Close()
    }()
    c.ws.SetReadLimit(maxMessageSize)
    c.ws.SetReadDeadline(time.Now().Add(pongWait))
    c.ws.SetPongHandler(func(string) error { 
        c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil
    })
    for {
        _, message, err := c.ws.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
                log.Printf("error: %v", err)
            }
            break
        }
        message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
        hub.broadcast <- message
    }
}

Source: https://github.com/gorilla/websocket/blob/a68708917c6a4f06314ab4e52493cc61359c9d42/examples/chat/conn.go#L50

This line

c.ws.SetPongHandler(func(string) error { 
    c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil
})

and this line

_, message, err := c.ws.ReadMessage()

seems to be not synchronized because the first line is a callback function so it should be invoked in a Goroutine created in the package and the second line is executing in the Goroutine that invoke serveWs

More importantly, how should I ensure that no more than one goroutine calls the SetReadDeadline, ReadMessage, SetPongHandler, SetPingHandler concurrently?

I tries to use a Mutex lock and lock it whenever I call the above functions, and unlock it afterwards, but quickly I realize a problem. It is usual (also in the example) that ReadMessage is being called in a for-loop. But if the Mutext is locked before the ReadMessage, then no other Read-functions can acquire the lock and execute until next message is received

Is there any better way in handling this concurrency issue? Thanks in advance.

dtc66318
dtc66318 在研究包内部之后,如果我没看错的话,ping和pong回调函数实际上是在与调用ReadMessage()相同的GoRoutine中执行的。因此该示例仍然保证了并发性。而且SetPingHandler()和SetPongHandler()也需要同步,但是在示例中,它们在调用任何ReadMessage()之前被调用,所以可以。
大约 3 年之前 回复

1个回答



确保没有并发调用read方法的最佳方法是从单个goroutine执行所有read方法。 </ p>

所有Gorilla网络套接字示例都使用这种方法,包括问题中粘贴的示例。 在该示例中,对read方法的所有调用均来自 readPump </ code>方法。 对于单个goroutine上的连接,调用 readPump </ code>方法一次。 因此,不会同时调用连接读取方法。</ p>

部分说,应用程序必须读取连接才能处理控制消息。 基于此以及Gorilla自己的示例,我认为可以安全地假定将像当前实现中那样从应用程序的读取goroutine中调用ping,pong和close处理程序。 如果文档可以更明确地说明这一点,那将是很好的。 也许有问题?</ p>
</ div>

展开原文

原文

The best way to ensure that there are no concurrent calls to the read methods is to execute all of the read methods from a single goroutine.

All of the Gorilla websocket examples use this approach including the example pasted in the question. In the example, all calls to the read methods are from the readPump method. The readPump method is called once for a connection on a single goroutine. It follows that the connection read methods are not called concurrently.

The section of the documentation on control messages says that the application must read the connection to process control messages. Based on this and Gorilla's own examples, I think it's safe to assume that the ping, pong and close handlers will be called from the application's reading goroutine as it is in the current implementation. It would be nice if the documentation could be more explicit about this. Maybe file an issue?

Csdn user default icon
上传中...
上传图片
插入图片
抄袭、复制答案,以达到刷声望分或其他目的的行为,在CSDN问答是严格禁止的,一经发现立刻封号。是时候展现真正的技术了!
其他相关推荐
将golang与大猩猩websocket库一起使用,为什么WriteJson返回错误?

<div class="post-text" itemprop="text"> <p>I'm following the instructions here:</p> <p><a href="https://testnet.bitmex.com/app/wsAPI" rel="nofollow noreferrer">https://testnet.bitmex.com/app/wsAPI</a></p> <p>and I've confirmed that the following Python implementation works (ie no network issues etc from my end) because: </p> <pre><code>python wsdump.py \ wss://testnet.bitmex.com/realtime &gt; {"op":"subscribe","args":["orderBookL2_25:XBTUSD"]} </code></pre> <p>results in</p> <pre><code>{"success":true,"subscribe":"orderBookL2_25:XBTUSD","request":{"op":"subscribe","args":["orderBookL2_25:XBTUSD"]}} </code></pre> <p>I've tried to modify the gorilla sample code to put together a basic client:</p> <pre><code>package main import ( "encoding/json" "flag" "fmt" "log" "net/url" "os" "os/signal" "time" "github.com/gorilla/websocket" ) var addr = flag.String("addr", "testnet.bitmex.com", "http service address") func main() { flag.Parse() log.SetFlags(0) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) u := url.URL{Scheme: "wss", Host: *addr, Path: "/realtime"} log.Printf("connecting to %s", u.String()) c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatal("dial:", err) } defer c.Close() done := make(chan struct{}) go func() { defer close(done) for { _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) return } log.Printf("recv: %s", message) } }() type commandStruct struct { Op string `json:"op"` Args []string `json:"args"` } command := &amp;commandStruct{ Op: "subscribe", Args: []string{"orderBookL2_25:XBTUSD"}} json, _ := json.Marshal(command) stringJSON := string(json) fmt.Println("JSON:", stringJSON) connectionErr := c.WriteJSON(stringJSON) if connectionErr != nil { log.Println("write:", connectionErr) } for { select { case &lt;-done: return case &lt;-interrupt: log.Println("interrupt") // Cleanly close the connection by sending a close message and then // waiting (with timeout) for the server to close the connection. err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { log.Println("write close:", err) return } select { case &lt;-done: case &lt;-time.After(time.Second): } return } } } </code></pre> <p>My program prints the following to the console:</p> <pre><code>{"op":"subscribe","args":["orderBookL2_25:XBTUSD"]} </code></pre> <p>which is identical to the JSON in the Python example above. But then I get an error message:</p> <pre><code>recv: {"status":400,"error":"Unrecognized request. See the docs or send 'help' for more details.","meta":{},"request":"{\"op\":\"subscribe\",\"args\":[\"orderBookL2_25:XBTUSD\"]}"} </code></pre> <p>Why do I get a different result?</p> </div>

Golang:通过WebSocket转发SSH

<div class="post-text" itemprop="text"> <p>I've been able to forward telnet over a websocket using golang, using something like</p> <pre><code>func forwardtcp(wsconn *websocket.Conn, conn *telnet.Conn) { connbuf := bufio.NewReader(conn) tcpbuffer := make([]byte, 128) for { n, err := connbuf.Read(tcpbuffer) if err != nil { log.Println("TCP Read failed") break } if err == nil { wsconn.WriteMessage(websocket.BinaryMessage, tcpbuffer[:n]) } } } </code></pre> <p>However I'm unable to do similar with an SSH or shell session. I'm not understanding a fundamental concept with the using the </p> <pre><code>targetStdout, _ := session.StdoutPipe() targetStdin, _ := session.StdinPipe() </code></pre> <p>pieces.</p> <p>I am able to use io.Copy, but not sure how to format these into a datagram that can be sent with the websocket connection.</p> <p>Is it possible to treat the targetStdin and targetStdout pipes in a manner that they can be read and written to with bytes, such as those received from the websocket connection? Or is there a better approach to get io from the SSH connection?</p> </div>

通过Golang中的Websocket发送和接收[] []字节

<div class="post-text" itemprop="text"> <p>I have a two-dimensional byte array:</p> <pre><code>data := [][]byte{{104, 105}, {104, 105}} </code></pre> <p>which I need to transport through a websocket connection, but I don't find a way how to convert this array so I can efficiently reconvert it back to a two-dimensional array on my java client.</p> <pre><code>func socketManager (connection *websocket.Conn ){ fmt.Print("Websocket connection established ") //determining the request for { //awaiting messages from clients messageType, message, err := connection.ReadMessage() if err != nil { //ending connection when error accursed Logger.LogError(err.(error).Error()) break } data := [][]byte{{104, 105}, {104, 105}} //How can I send this two dimensional byte array? err = connection.WriteMessage(websocket.BinaryMessage, message) if err != nil { log.Println("write:", err) break } err = connection.WriteMessage(messageType, message) if err != nil { log.Println("write:", err) break } } //closing the connection defer connection.Close() } </code></pre> </div>

Golang Gorilla Websocket在120秒后停止接收信息

<div class="post-text" itemprop="text"> <p>I'm currently trying to connect to the CEX.IO bitcoin exchange's websocket, but have been having issues not only with CEX.IO but with others too. All of my connections drop around the 120-second mark which makes me think there is some TTL problem going on. The Process() goroutine in the main package ends up just hanging and waiting for data from the readLoop which just stops receiving data. I've included some read-only API keys in the code so you can test if you'd like.</p> <pre><code>package main import ( "fmt" "bitbucket.org/tradedefender/cryptocurrency/exchange-connector/cexio" "github.com/shopspring/decimal" "encoding/json" "time" ) type OrderBook struct { Asks []Ask Bids []Bid } type Ask struct { Rate decimal.Decimal Amount decimal.Decimal } type Bid struct { Rate decimal.Decimal Amount decimal.Decimal } func main() { cexioConn := new(cexio.Connection) err := cexioConn.Connect() if err != nil { fmt.Errorf("error: %s", err.Error()) } err = cexioConn.Authenticate("TLwYkktLf7Im6nqSKt6UO1IrU", "9ImOJcR7Qj3LMIyPCzky0D7WE") if err != nil { fmt.Errorf("error: %s", err.Error()) } readChannel := make(chan cexio.IntraAppMessage, 25) go cexioConn.ReadLoop(readChannel) processor := Processor{ WatchPairs: [][2]string{ [2]string{ "BTC", "USD", }, }, conn: cexioConn, } go processor.Process(readChannel) // LOL for { continue } } type Processor struct { WatchPairs [][2]string conn *cexio.Connection } func (p *Processor) Process(ch &lt;-chan cexio.IntraAppMessage) { p.conn.SubscribeToOrderBook(p.WatchPairs[0]) pingTimer := time.Now().Unix() for { fmt.Printf("(%v) ", time.Now().Unix()) if (time.Now().Unix() - pingTimer) &gt;= 10 { fmt.Println("sending ping") p.conn.SendPing() pingTimer = time.Now().Unix() } readMsg := &lt;- ch output, _ := json.Marshal(readMsg.SocketMessage) fmt.Println(string(output)) if readMsg.SocketMessage.Event == "ping" { fmt.Println("sending pong") p.conn.SendPong() pingTimer = time.Now().Unix() } } } </code></pre> <p>Below is the connector to the cexio websocket. Here is a link to their API: <a href="https://cex.io/websocket-api" rel="nofollow noreferrer">https://cex.io/websocket-api</a></p> <pre><code>package cexio import ( "github.com/gorilla/websocket" //"github.com/shopspring/decimal" "github.com/satori/go.uuid" "encoding/hex" "encoding/json" "crypto/hmac" "crypto/sha256" "bytes" "strconv" "time" "fmt" ) const Url = "wss://ws.cex.io/ws/" type Connection struct { conn *websocket.Conn } type IntraAppMessage struct { SocketMessage GenericMessage ProgramMessage ProgramMessage } type GenericMessage struct { Event string `json:"e"` Data interface{} `json:"data"` Auth AuthData `json:"auth,omitempty"` Ok string `json:"ok,omitempty"` Oid string `json:"oid,omitempty"` Time int64 `json:"time,omitempty"` } type ProgramMessage struct { Error string } type AuthData struct { Key string `json:"key"` Signature string `json:"signature"` Timestamp int64 `json:"timestamp"` } type OrderBookSubscribeData struct { Pair [2]string `json:"pair"` Subscribe bool `json:"subscribe"` Depth int `json:"depth"` } func (c *Connection) SendPong() error { pongMsg := GenericMessage{ Event: "pong", } err := c.conn.WriteJSON(pongMsg) if err != nil { return nil } deadline := time.Now().Add(15*time.Second) err = c.conn.WriteControl(websocket.PongMessage, nil, deadline) if err != nil { return err } return nil } func (c *Connection) SendPing() error { pingMsg := GenericMessage{ Event: "get-balance", Oid: uuid.NewV4().String(), } err := c.conn.WriteJSON(pingMsg) if err != nil { return err } deadline := time.Now().Add(15*time.Second) err = c.conn.WriteControl(websocket.PingMessage, nil, deadline) if err != nil { return err } return nil } func (c *Connection) Connect() error { dialer := *websocket.DefaultDialer wsConn, _, err := dialer.Dial(Url, nil) if err != nil { return err } c.conn = wsConn //c.conn.SetPingHandler(c.HandlePing) for { _, msgBytes, err := c.conn.ReadMessage() if err != nil { c.Disconnect() return err } fmt.Println(string(msgBytes)) var m GenericMessage err = json.Unmarshal(msgBytes, &amp;m) if err != nil { c.Disconnect() return err } if m.Event != "connected" { c.Disconnect() return err } else { break } } return nil } func (c *Connection) Disconnect() error { return c.conn.Close() } func (c *Connection) ReadLoop(ch chan&lt;- IntraAppMessage) { for { fmt.Println("starting new read") _, msgBytes, err := c.conn.ReadMessage() if err != nil { ch &lt;- IntraAppMessage{ ProgramMessage: ProgramMessage{ Error: err.Error(), }, } continue } var m GenericMessage err = json.Unmarshal(msgBytes, &amp;m) if err != nil { ch &lt;- IntraAppMessage{ ProgramMessage: ProgramMessage{ Error: err.Error(), }, } continue } ch &lt;- IntraAppMessage{ SocketMessage: m, } } } func CreateSignature(timestamp int64, key, secret string) string { secretBytes := []byte(secret) h := hmac.New(sha256.New, secretBytes) var buffer bytes.Buffer buffer.WriteString(strconv.FormatInt(timestamp, 10)) buffer.WriteString(key) h.Write(buffer.Bytes()) return hex.EncodeToString(h.Sum(nil)) } func (c *Connection) Authenticate(key, secret string) error { timestamp := time.Now().Unix() signature := CreateSignature(timestamp, key, secret) var authMsg GenericMessage authMsg.Event = "auth" authMsg.Auth = AuthData{ Key: key, Signature: signature, Timestamp: timestamp, } err := c.conn.WriteJSON(authMsg) if err != nil { return err } for { _, msgBytes, err := c.conn.ReadMessage() if err != nil { c.Disconnect() return err } fmt.Println(string(msgBytes)) var m GenericMessage err = json.Unmarshal(msgBytes, &amp;m) if err != nil { c.Disconnect() return err } if m.Event != "auth" &amp;&amp; m.Ok != "ok" { c.Disconnect() return err } else { break } } return nil } func (c *Connection) SubscribeToOrderBook(pair [2]string) error { sendMsg := GenericMessage{ Event: "order-book-subscribe", Data: OrderBookSubscribeData{ Pair: pair, Subscribe: true, Depth: 0, }, Oid: uuid.NewV4().String(), } err := c.conn.WriteJSON(sendMsg) if err != nil { return err } return nil } func (c *Connection) GetBalance() error { sendMsg := GenericMessage{ Event: "get-balance", Oid: uuid.NewV4().String(), } err := c.conn.WriteJSON(sendMsg) if err != nil { return err } return nil } </code></pre> </div>

Golang WebSocket的SetWriteDeadline函数

<div class="post-text" itemprop="text"> <p>Go's websockets have a <strong>SetWriteDeadline()</strong> function to set the connection's network write deadline. Do I need to set it before every data sending or it can be done just once when the connection is created?</p> </div>

Golang WebSocket客户端

<div class="post-text" itemprop="text"> <p>I want to make client websocket connections to exertnal server each connection = goroutine and reader. I was looking informations on the internet but I found how to create server websocket tutorials. Can anyone be so kind and make a trivial example and walk me through. I am using standart golang libary <a href="https://golang.org/x/net/websocket" rel="nofollow">https://golang.org/x/net/websocket</a>.</p> <p>I created some code but when I closed one connection program exited with EOF information. I won't post the code because it's probably bad duo to the fact it was my first try.</p> <p>I know how to read/send message from websocket but I don't know how to create multiple connections.</p> <p>Any informations, examples would be appreciate, thanks for reading</p> </div>

带有Web代理的Golang Gorilla Websocket

<div class="post-text" itemprop="text"> <p>I'm working on a POC using Gorilla/Websocket to communicate, through a Web Proxy, with the Websocket Check Website "echo.websocket.org".</p> <p>I'm using the free online Proxy "hide.me/en/" for testing.</p> <p>When I simply try to communicate with "echo.websocket.org" (Server side), my client-site Websocket POC reach the response.</p> <p>But when i try to add the Proxy gesture, everything goes wrong :(</p> <p>Here is a sample of my code :</p> <pre><code>package main import ( "flag" "log" "net/url" "os" "os/signal" "time" "github.com/gorilla/websocket" "net/http" ) var addrWebsocket = flag.String("addrWebsocket", "echo.websocket.org", "http service address") func main() { flag.Parse() log.SetFlags(0) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) //Initialize the WebSocket URL and the Path to follow uWS := url.URL{Scheme: "wss", Host: *addrWebsocket} //Initialize the Proxy URL and the Path to follow uProxy, _ := url.Parse("https://hide.me/en/proxy") //Set the Dialer (especially the proxy) dialer := websocket.Dialer{ Proxy: http.ProxyURL(uProxy), } //dialer := websocket.DefaultDialer ==&gt; with this default dialer, it works ! c, _, err := dialer.Dial(uWS.String(), nil) // ==&gt; With the proxy config, it fails here ! defer c.Close() done := make(chan struct{}) go func() { defer c.Close() defer close(done) for { _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) return } log.Printf("recv: %s", message) } }() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case t := &lt;-ticker.C: err := c.WriteMessage(websocket.TextMessage, []byte(t.String())) if err != nil { log.Println("write:", err) return } case &lt;-interrupt: log.Println("interrupt") err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { log.Println("write close:", err) return } select { case &lt;-done: case &lt;-time.After(time.Second): } c.Close() return } } } </code></pre> <p>I'm a very beginner in Go so don't hesitate to correct me if my code un not clear and clean.</p> <p>Thanks for your help !</p> </div>

如何在golang中的所有包中访问全局常量?

<div class="post-text" itemprop="text"> <p>I have a constant defined within a package in golang as such:</p> <pre><code>package services const ( Source = "local" ) </code></pre> <p>I would like to make this constant accessible by other packages without having to import the package into my other module. How can I go about that?</p> </div>

Golang Websocket客户端关闭了连接

<div class="post-text" itemprop="text"> <p>I have a golang connection. If the client opened the connection the database change the online column to 1. Here is my code but I didnt implement the last thing. My question, how can I change the database column to 0 if the client closed the connection. How can I track the client, and catch the connection close? </p> <pre><code>package main import ( "golang.org/x/net/websocket" "fmt" "log" "net/http" "strconv" ) type Clients struct { webs *websocket.Conn clientAddr string id int } var ( ActiveClients = make(map[Clients]int) ) func Echo(ws *websocket.Conn){ var err error client := ws.Request().RemoteAddr sockCli := Clients{webs: ws, clientAddr: client, id: 0} ActiveClients[sockCli] = 1 for{ var reply string if err = websocket.Message.Receive(ws, &amp;reply); err != nil{ fmt.Println("Cant receive", err) break }else{ if sockCli.id == 0{ sockCli.id, err = strconv.Atoi(reply) } } if err = websocket.Message.Send(ws, "id:"+strconv.Itoa(sockCli.id)); err != nil{ fmt.Println("Cant send") } for cs, _ := range ActiveClients { if cs.id == 1{ if err = websocket.Message.Send(cs.webs, "asd"); err != nil { // we could not send the message to a peer log.Println("Could not send message to ", cs.clientAddr, err.Error()) } }else { fmt.Println("No") } } } } func main(){ http.Handle("/", websocket.Handler(Echo)) if err := http.ListenAndServe(":1234", nil); err != nil{ log.Fatal("ListenAndServe: ", err) } } </code></pre> </div>

使用Golang扩展WebSocket连接

<div class="post-text" itemprop="text"> <pre><code> func handleConn(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { if _, ok := err.(websocket.HandshakeError); !ok { log.Println(err) } return } go writer(ws) reader(r, ws) } func main() { http.HandleFunc("/", handleConn) } </code></pre> <p>I am trying to build a high scaling websocket server using golang. Since there is lack of support in native go websocket package, i am using <a href="https://github.com/gorilla/websocket" rel="nofollow noreferrer">https://github.com/gorilla/websocket</a>. </p> <p>For every connection, a goroutine will be spawned to take care of writes to it. For scaling huge number of connections. Let's say if I have 1M concurrent connections then there should be 1M goroutines will be running on the server.</p> <p><strong>Hardware Specification:</strong></p> <p>16 GB Ram</p> <p>4 Core CPU</p> <p>2.5 GHz Intel Core i5</p> <p>will it work for large number of connections without affecting the performance?</p> </div>

使用Golang的gorilla / websocket软件包的Websocket连接断开

<div class="post-text" itemprop="text"> <p>I'm attempting to port a NodeJS script that establishes and maintains a Websocket connection to a third-party server to Go using the <a href="https://github.com/gorilla/websocket" rel="nofollow noreferrer">gorilla/websocket</a> package. In the <a href="https://gist.github.com/anonymous/7c169e995d13a943d3f494c88cac7a17" rel="nofollow noreferrer">Node script</a>, a pong is received following a ping, and the connection is kept alive indefinitely. In the <a href="https://gist.github.com/anonymous/69b8dca0172803fccb45dfeb5d310133" rel="nofollow noreferrer">Go script</a>, ping/pong works but the connection is dropped by the server after about 30 seconds. </p> <p>I suspect that the pings that are sent using the Go websocket package are malformed, but I can't pinpoint the cause of this. Comparing the captured, encrypted network traffic while running these programs shows no difference in the response length of the TCP requests and responses, so this may not be the issue. Any help would be greatly appreciated!</p> <p><strong>websocket.js</strong></p> <pre><code>#!/usr/bin/env node // npm install websocket@1.0.25 --save const WebSocketClient = require('websocket').client; const client = new WebSocketClient(); let lastPing = new Date().getTime(); client.on('connectFailed', function(error) { console.log('Connect Error: ' + error.toString()); }); client.on('connect', function(connection) { console.log('Connected to Server...'); connection.on('error', function(error) { console.log("Connection Error: " + error.toString()); }); connection.on('close', function() { console.log('Connection Closed'); }); connection.on('message', function(message) { if (message.type === 'utf8') { console.log(message.utf8Data); } }); connection.on('pong', function(){ console.log('[pingpong] response took', (new Date().getTime() - lastPing) + 'ms'); }) function send(message) { if (connection.connected) { connection.sendUTF(message); } } // Send a ping every 10s // to keep the connection live setInterval(function(){ lastPing = new Date().getTime(); connection.ping(); }, 10000); }); client.connect('wss://ws.radarrelay.com/0x/v0/ws'); </code></pre> <p><strong>websocket.go</strong></p> <pre><code>package main import ( "flag" "log" "os" "os/signal" "time" "github.com/gorilla/websocket" ) var addr = "wss://api.radarrelay.com/0x/v0/ws" func main() { flag.Parse() log.SetFlags(0) timeoutDuration := 2 * time.Minute interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) c, _, err := websocket.DefaultDialer.Dial(addr, nil) if err != nil { log.Fatal("dial:", err) } else { log.Println("Connected to server") } c.SetPongHandler(func(str string) error { log.Println("pong received", str) return nil }) defer c.Close() done := make(chan struct{}) go func() { defer c.Close() defer close(done) for { c.SetReadDeadline(time.Now().Add(timeoutDuration)) _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) return } if len(message) &gt;= 2 { message = message[2:] } log.Printf("recv: %s", message) } }() ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case _ = &lt;-ticker.C: err := c.WriteMessage(websocket.PingMessage, []byte{}) if err != nil { log.Println("write:", err) return } else { log.Println("ping sent") } case &lt;-interrupt: log.Println("interrupt") // To cleanly close a connection, a client should send a close // frame and wait for the server to close the connection. err := c.WriteMessage( websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { log.Println("write close:", err) return } select { case &lt;-done: case &lt;-time.After(time.Second): } c.Close() return } } } </code></pre> </div>

Golang Server关闭客户端的连接:websocket

<div class="post-text" itemprop="text"> <p>i have a problem with my golang server in which i'm using websockets. The server opens the connection and the client could connect to it, but the problem is that when the server starts sending the data to the client, the client connection is closed after a small period of time. i suggest that the problem is with the server and not with the client because i tried to connect to the server with another web client, and it's the same issue. I didn't understand the cause ! Can someone help me?</p> <p>server.go:</p> <pre><code> func Echo(ws *websocket.Conn) { fmt.Println("Echoing") for { msg := MessageReceived{Name: "OrderCommand", Nbmsg: 3} if err := websocket.JSON.Send(ws, msg); err != nil { fmt.Println("Can't send") break } //os.Exit(0) } } func checkError(err error) { if err != nil { Log("Fatal error ", err.Error()) os.Exit(1) } } func main() { http.HandleFunc("/save", saveHandler) http.Handle("/", websocket.Handler(Echo)) err:= http.ListenAndServe(":8081", nil) checkError(err) } </code></pre> <p>and client.go:</p> <pre><code> import ( "code.google.com/p/go.net/websocket" "fmt" "log" ) func main() { origin := "http://localhost/" url := "ws://localhost:8081/echo" ws, err := websocket.Dial(url, "", origin) if err != nil { log.Fatal(err) } var msg = make([]byte, 512) var n int if n, err = ws.Read(msg); err != nil { log.Fatal(err) } fmt.Printf("Received: %s. ", msg[:n]) } </code></pre> </div>

Golang WebSocket数据不正确

<div class="post-text" itemprop="text"> <p>I'm using <code>x.net.websocket</code> to read data from a websocket. The data is relatively large. When I read it, I can't read it completely, so it is cut off. Is there any way to solve it? </p> <pre><code>func receiveWebsocket(ws *websocket.Conn) error { for { var msg = make([]byte, 1024*1024) // 1024kb m, err := ws.Read(msg) if err != nil { log15.Error("ws read error", "error", err) return err } fmt.Println("length ---",m, string(msg)) response := string(msg[:m]) assignmentWebsocket(response) } } </code></pre> <p>According to the log, the <code>m</code> value is always 4092, even if <code>msg</code> is very large.</p> </div>

Golang-扩展websocket客户端以实现与不同服务器的多个连接

<div class="post-text" itemprop="text"> <p>I have a websocket client. In reality, it is far more complex than the basic code shown below. I now need to scale this client code to open connections to multiple servers. Ultimately, the tasks that need to be performed when a message is received from the servers is identical. What would be the best approach to handle this? As I said above the actual code performed when receiving the message is far more complex than shown in the example.</p> <pre><code>package main import ( "flag" "log" "net/url" "os" "os/signal" "time" "github.com/gorilla/websocket" ) var addr = flag.String("addr", "localhost:1234", "http service address") func main() { flag.Parse() log.SetFlags(0) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) // u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"} u := url.URL{Scheme: "ws", Host: *addr, Path: "/"} log.Printf("connecting to %s", u.String()) c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatal("dial:", err) } defer c.Close() done := make(chan struct{}) go func() { defer close(done) for { _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) return } log.Printf("recv: %s", message) } }() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case &lt;-done: return case t := &lt;-ticker.C: err := c.WriteMessage(websocket.TextMessage, []byte(t.String())) if err != nil { log.Println("write:", err) return } case &lt;-interrupt: log.Println("interrupt") // Cleanly close the connection by sending a close message and then // waiting (with timeout) for the server to close the connection. err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { log.Println("write close:", err) return } select { case &lt;-done: case &lt;-time.After(time.Second): } return } } } </code></pre> </div>

golang在一个包中允许多个init的目的是什么?

<div class="post-text" itemprop="text"> <p>I know that golang allows multiple init in one package and even in one file. I am wondering why? For example, if a pkg has many files, we could write multiple init then we could get lost in where to we should put init, and we could be also confused about the init order if we have multiple init in one pkg. (I mean is this better? we can only have 1 init, then we can have some initXXX, then put them into init, it seems quite clean.) What's the advantage of doing this in code struct view?</p> </div>

Gorilla WebSocket与golang.org/x/net/websocket的比较

<div class="post-text" itemprop="text"> <p>According <a href="https://github.com/gorilla/websocket#gorilla-websocket-compared-with-other-packages" rel="nofollow">Gorilla Websockets Project</a> it is not possible to send pings and pongs using <strong>golang.org/x/net/websocket</strong>. At the same time, the following is on the project page of <a href="http://godoc.org/golang.org/x/net/websocket" rel="nofollow">golang.org/x/net/websocket</a>:</p> <blockquote> <p>Package websocket implements a client and server for the WebSocket protocol as specified in RFC 6455.</p> </blockquote> <p>I am a little confused. <strong>golang.org/x/net/websocket</strong> implements RFC 6455 but can not send control frames (cancel, ping, pong) although this is specified in <a href="http://tools.ietf.org/html/rfc6455#section-5.5" rel="nofollow">RFC 6455 - Section Control Frames</a></p> <p>So what will happen if I use <strong>golang.org/x/net/websocket</strong> package. Will the connection abort after a timeout? In other words, how is it ensured here that the connection does not break off.</p> </div>

Golang WebSocket客户端,获取结果后关闭连接

<div class="post-text" itemprop="text"> <p>How I can implement this kind of scenario:</p> <p>1.I have LoginHandler which receives some user data - email and signedXml:</p> <pre><code>func LoginHandler(c *gin.Context) { var ( err error data LoginPost ) if err = c.BindJSON(&amp;data); err != nil { c.JSON(http.StatusBadRequest, gin.H{"status": "error"}) return } ... c.JSON(http.StatusOK, gin.H{"status": "ok"}) } </code></pre> <p>2.I need to send signedXml to another server via websocket</p> <p>3.Save result (success or error)</p> <p>4.Close connection</p> <p>Every HTTP request will open connection, send 1 message, get 1 result and finally close socket. I was trying with channel, but no success. Is this possible to implement my case?</p> <p><strong>UPDATE</strong></p> <pre><code>package main import ( "log" "net/url" "github.com/gorilla/mux" "github.com/gorilla/websocket" "net/http" ) func indexHandler(w http.ResponseWriter, r *http.Request) { message := r.FormValue("message") w.Write([]byte(message)) } func postHandler(w http.ResponseWriter, r *http.Request) { var ( message = r.FormValue("message") u = url.URL{Scheme: "ws", Host: "echo.websocket.org", Path: "/"} err error out []byte conn *websocket.Conn ) log.Printf("message: %s ", message) log.Printf("connecting to %s ", u.String()) conn, _, err = websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatal("dial:", err) } log.Println("write") if err = conn.WriteMessage(websocket.TextMessage, []byte(message)); err != nil { log.Fatal("write:", err) } log.Println("read") if _, out, err = conn.ReadMessage(); err != nil { log.Fatal("read:", err) } w.Write(out) log.Println("close") conn.Close() } func main() { r := mux.NewRouter() r.HandleFunc("/", indexHandler).Methods("GET") r.HandleFunc("/post", postHandler).Methods("POST") http.Handle("/", r) http.ListenAndServe(":8080", nil) } </code></pre> </div>

如何在Golang中释放WebSocket和Redis网关服务器资源?

<div class="post-text" itemprop="text"> <p>I have a gateway server, which can push message to client side by using websocket, A new client connected to my server, I will generate a <code>cid</code> for it. And then I also subscribe a channel, which using <code>cid</code>. If any message publish to this channel, My server will push it to client side. For now, all unit are working fine, but when I try to test with benchmark test by <a href="https://github.com/observing/thor" rel="nofollow noreferrer">thor</a>, it will crash, I fine the <code>DeliverMessage</code> has some issue, it would never exit, since it has a die-loop. but since redis need to subscribe something, I don't know how to avoid loop.</p> <pre><code>func (h *Hub) DeliverMessage(pool *redis.Pool) { conn := pool.Get() defer conn.Close() var gPubSubConn *redis.PubSubConn gPubSubConn = &amp;redis.PubSubConn{Conn: conn} defer gPubSubConn.Close() for { switch v := gPubSubConn.Receive().(type) { case redis.Message: // fmt.Printf("Channel=%q | Data=%s ", v.Channel, string(v.Data)) h.Push(string(v.Data)) case redis.Subscription: fmt.Printf("Subscription message: %s : %s %d ", v.Channel, v.Kind, v.Count) case error: fmt.Println("Error pub/sub, delivery has stopped", v) panic("Error pub/sub") } } } </code></pre> <p>In the main function, I have call the above function as:</p> <pre><code>go h.DeliverMessage(pool) </code></pre> <p>But when I test it with huge connection, it get me some error like:</p> <blockquote> <p>ERR max number of clients reached</p> </blockquote> <p>So, I change the redis pool size by change <code>MaxIdle</code>:</p> <pre><code>func newPool(addr string) *redis.Pool { return &amp;redis.Pool{ MaxIdle: 5000, IdleTimeout: 240 * time.Second, Dial: func() (redis.Conn, error) { return redis.Dial("tcp", addr) }, } } </code></pre> <p>But it still doesn't work, so I wonder to know, if there any good way to kill a goroutine after my websocket disconnected to my server on the below selection:</p> <pre><code>case client := &lt;-h.Unregister: if _, ok := h.Clients[client]; ok { delete(h.Clients, client) delete(h.Connections, client.CID) close(client.Send) if err := gPubSubConn.Unsubscribe(client.CID); err != nil { panic(err) } // TODO kill subscribe goroutine if don't client-side disconnected ... } </code></pre> <p>But How do I identify this goroutine? How can I do it like <code>unix</code> way. <code>kill -9 &lt;PID&gt;</code>?</p> </div>

如何从golang的websocket服务器向客户端主动发送消息

<div class="post-text" itemprop="text"> <p>I'm a newcomer for the golang and websocket.</p> <p>I'm trying to write a websocket server which can send the messages actively to client once the handshake is done.</p> <p>but my server will just only send the message to the client when got the request from the client.</p> <p>Does anyone know how to implement this feature or where can I find the related answer for that? </p> <p>Thank you so much.</p> <p>the source code is as follows:</p> <pre><code>package main import ( "log" "net/http" ) func handler(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Write([]byte("Hi, the handshake is completed. ")) w.Write([]byte("Let's start to talk something. ")) } func main() { http.HandleFunc("/", handler) log.Printf("Start to listen on 443.") err := http.ListenAndServeTLS(":443", "server.crt", "server.key", nil) log.Fatal(err) } </code></pre> </div>

如果能重来,我不会选择北漂——初见北京

一个人走的路

技术大佬:我去,你写的 switch 语句也太老土了吧

昨天早上通过远程的方式 review 了两名新来同事的代码,大部分代码都写得很漂亮,严谨的同时注释也很到位,这令我非常满意。但当我看到他们当中有一个人写的 switch 语句时,还是忍不住破口大骂:“我擦,小王,你丫写的 switch 语句也太老土了吧!” 来看看小王写的代码吧,看完不要骂我装逼啊。 private static String createPlayer(PlayerTypes p...

副业收入是我做程序媛的3倍,工作外的B面人生是怎样的?

提到“程序员”,多数人脑海里首先想到的大约是:为人木讷、薪水超高、工作枯燥…… 然而,当离开工作岗位,撕去层层标签,脱下“程序员”这身外套,有的人生动又有趣,马上展现出了完全不同的A/B面人生! 不论是简单的爱好,还是正经的副业,他们都干得同样出色。偶尔,还能和程序员的特质结合,产生奇妙的“化学反应”。 @Charlotte:平日素颜示人,周末美妆博主 大家都以为程序媛也个个不修边幅,但我们也许...

我说我不会算法,阿里把我挂了。

不说了,字节跳动也反手把我挂了。

2020年大厂Java面试前复习的正确姿势(800+面试题答案解析)

前言 个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、 丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油! 本篇分享的面试题内容包括:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Redis、MySQL、Spring、SpringBoot、SpringCloud、RabbitMQ...

抖音上很火的时钟效果

反正,我的抖音没人看,别人都有几十万个赞什么的。 发到CSDN上来,大家交流下~ 主要用到原生态的 JS+CSS3。 具体不解释了,看注释: &lt;!DOCTYPE html&gt; &lt;html lang="en"&gt; &lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;title&gt;Title&lt;/tit...

记录下入职中软一个月(外包华为)

我在年前从上一家公司离职,没想到过年期间疫情爆发,我也被困在家里,在家呆着的日子让人很焦躁,于是我疯狂的投简历,看面试题,希望可以进大公司去看看。 我也有幸面试了我觉得还挺大的公司的(虽然不是bat之类的大厂,但是作为一名二本计算机专业刚毕业的大学生bat那些大厂我连投简历的勇气都没有),最后选择了中软,我知道这是一家外包公司,待遇各方面甚至不如我的上一家公司,但是对我而言这可是外包华为,能...

又出事了?网站被攻击了?高中生?

北京时间2020年3月27日9点整,如往常一样来到公司,带开电脑,正准备打开Github网站看一会源代码,再开始手头的工作。哟吼,一直打不开,一直出现如下页面: 我想很多网友也尝到了甜头,各大技术群炸开了锅,据网友反馈有攻击者正在发起大规模的中间人挟持,京东和Github等网站等网站都受到了影响。 什么是中间中间人挟持呢? 简而言之,就是攻击者在数据网络传输的过程中,截获传输过程中的数据并篡改...

培训班出来的人后来都怎么样了?(二)

接着上回说,培训班学习生涯结束了。后面每天就是无休止的背面试题,不是没有头脑的背,培训公司还是有方法的,现在回想当时背的面试题好像都用上了,也被问到了。回头找找面试题,当时都是打印下来天天看,天天背。 不理解呢也要背,面试造飞机,上班拧螺丝。班里的同学开始四处投简历面试了,很快就有面试成功的,刚开始一个,然后越来越多。不知道是什么原因,尝到胜利果实的童鞋,不满足于自己通过的公司,嫌薪水要少了,选择...

面试了一个 31 岁程序员,让我有所触动,30岁以上的程序员该何去何从?

最近面试了一个31岁8年经验的程序猿,让我有点感慨,大龄程序猿该何去何从。

大三实习生,字节跳动面经分享,已拿Offer

说实话,自己的算法,我一个不会,太难了吧

程序员垃圾简历长什么样?

已经连续五年参加大厂校招、社招的技术面试工作,简历看的不下于万份 这篇文章会用实例告诉你,什么是差的程序员简历! 疫情快要结束了,各个公司也都开始春招了,作为即将红遍大江南北的新晋UP主,那当然要为小伙伴们做点事(手动狗头)。 就在公众号里公开征简历,义务帮大家看,并一一点评。《启舰:春招在即,义务帮大家看看简历吧》 一石激起千层浪,三天收到两百多封简历。 花光了两个星期的所有空闲时...

工作八年,月薪60K,裸辞两个月,投简历投到怀疑人生!

近日,有网友在某职场社交平台吐槽,自己裸辞两个月了,但是找工作却让自己的心态都要崩溃了,全部无果,不是已查看无回音,就是已查看不符合。 “工作八年,两年一跳,裸辞两个月了,之前月薪60K,最近找工作找的心态崩了!所有招聘工具都用了,全部无果,不是已查看无回音,就是已查看不符合。进头条,滴滴之类的大厂很难吗???!!!投简历投的开始怀疑人生了!希望 可以收到大厂offer” 先来看看网...

我把华为小米年报放一起,发现华为才是真·手机公司,小米确实不靠卖手机赚钱...

郭一璞 发自 凹非寺量子位 报道 | 公众号 QbitAI国产手机界的两大玩家,华为&amp;小米,昨天在同一天前后脚发布了2019年财报。同行冤家,发财报也碰在了同一天。那我们就对比...

大牛都会用的IDEA调试技巧!!!

导读 前天面试了一个985高校的实习生,问了他平时用什么开发工具,他想也没想的说IDEA,于是我抛砖引玉的问了一下IDEA的调试用过吧,你说说怎么设置断点...

97年世界黑客编程大赛冠军作品(大小仅为16KB),惊艳世界的编程巨作

这是世界编程大赛第一名作品(97年Mekka ’97 4K Intro比赛)汇编语言所写。 整个文件只有4095个字节, 大小仅仅为16KB! 不仅实现了3D动画的效果!还有一段震撼人心的背景音乐!!! 内容无法以言语形容,实在太强大! 下面是代码,具体操作看最后! @echo off more +1 %~s0|debug e100 33 f6 bf 0 20 b5 10 f3 a5...

不要再到处使用 === 了

我们知道现在的开发人员都使用 === 来代替 ==,为什么呢?我在网上看到的大多数教程都认为,要预测 JavaScript 强制转换是如何工作这太复杂了,因此建议总是使用===。这些都...

什么是a站、b站、c站、d站、e站、f站、g站、h站、i站、j站、k站、l站、m站、n站?00后的世界我不懂!

A站 AcFun弹幕视频网,简称“A站”,成立于2007年6月,取意于Anime Comic Fun,是中国大陆第一家弹幕视频网站。A站以视频为载体,逐步发展出基于原生内容二次创作的完整生态,拥有高质量互动弹幕,是中国弹幕文化的发源地;拥有大量超粘性的用户群体,产生输出了金坷垃、鬼畜全明星、我的滑板鞋、小苹果等大量网络流行文化,也是中国二次元文化的发源地。 B站 全称“哔哩哔哩(bilibili...

十个摸鱼,哦,不对,是炫酷(可以玩一整天)的网站!!!

文章目录前言正文**1、Kaspersky Cyberthreat real-time map****2、Finding Home****3、Silk – Interactive Generative Art****4、Liquid Particles 3D****5、WINDOWS93****6、Staggering Beauty****7、Ostagram图片生成器网址****8、全历史网址*...

终于,月薪过5万了!

来看几个问题想不想月薪超过5万?想不想进入公司架构组?想不想成为项目组的负责人?想不想成为spring的高手,超越99%的对手?那么本文内容是你必须要掌握的。本文主要详解bean的生命...

毕业5年,我熬夜整理出了这50个优质的电子书网站,吐血推荐!

大家好,我是武哥,最近经常有小伙伴问我要电子书,都什么年代了,还找不到电子书吗?如果要说原因,那就是你还没遇到武哥我(手动滑稽~)!我今天把这么多年我经常看的电子书网站整理一下给大家,基本上能解决大家的需求。不管是在校生还是已经工作了,相信肯定对你有所帮助! 1.鸠摩搜书 首先给大家推荐的网站是:鸠摩搜书 地址:https://www.jiumodiary.com/ 这个网上非常棒,上面有很多优质...

MySQL性能优化(五):为什么查询速度这么慢

前期回顾: MySQL性能优化(一):MySQL架构与核心问题 MySQL性能优化(二):选择优化的数据类型 MySQL性能优化(三):深入理解索引的这点事 MySQL性能优化(四):如何高效正确的使用索引 前面章节我们介绍了如何选择优化的数据类型、如何高效的使用索引,这些对于高性能的MySQL来说是必不可少的。但这些还完全不够,还需要合理的设计查询。如果查询写的很糟糕,即使表结构再合理、索引再...

大厂的 404 页面都长啥样?最后一个笑了...

每天浏览各大网站,难免会碰到404页面啊。你注意过404页面么?猿妹搜罗来了下面这些知名网站的404页面,以供大家欣赏,看看哪个网站更有创意: 正在上传…重新上传取消 腾讯 正在上传…重新上传取消 网易 淘宝 百度 新浪微博 正在上传…重新上传取消 新浪 京东 优酷 腾讯视频 搜...

自从喜欢上了B站这12个UP主,我越来越觉得自己是个废柴了!

不怕告诉你,我自从喜欢上了这12个UP主,哔哩哔哩成为了我手机上最耗电的软件,几乎每天都会看,可是吧,看的越多,我就越觉得自己是个废柴,唉,老天不公啊,不信你看看…… 间接性踌躇满志,持续性混吃等死,都是因为你们……但是,自己的学习力在慢慢变强,这是不容忽视的,推荐给你们! 都说B站是个宝,可是有人不会挖啊,没事,今天咱挖好的送你一箩筐,首先啊,我在B站上最喜欢看这个家伙的视频了,为啥 ,咱撇...

代码注释如此沙雕,会玩还是你们程序员!

某站后端代码被“开源”,同时刷遍全网的,还有代码里的那些神注释。 我们这才知道,原来程序员个个都是段子手;这么多年来,我们也走过了他们的无数套路… 首先,产品经理,是永远永远吐槽不完的!网友的评论也非常扎心,说看这些代码就像在阅读程序员的日记,每一页都写满了对产品经理的恨。 然后,也要发出直击灵魂的质问:你是尊贵的付费大会员吗? 这不禁让人想起之前某音乐app的穷逼Vip,果然,穷逼在哪里都是...

总结了Mybatis,原来知识点也没多少嘛

看完这篇Mybatis,感觉你三天就会用了。

爬虫(101)爬点重口味的

小弟最近在学校无聊的很哪,浏览网页突然看到一张图片,都快流鼻血。。。然后小弟冥思苦想,得干一点有趣的事情python 爬虫库安装https://s.taobao.com/api?_ks...

疫情后北上广深租房价格跌了吗? | Alfred数据室

去年3月份我们发布了《北上广深租房图鉴》(点击阅读),细数了北上广深租房的各种因素对租房价格的影响。一年过去了,在面临新冠疫情的后续影响、城市尚未完全恢复正常运转、学校还没开学等情况下...

面试官给我挖坑:a[i][j] 和 a[j][i] 有什么区别?

点击上方“朱小厮的博客”,选择“设为星标”后台回复&#34;1024&#34;领取公众号专属资料本文以一个简单的程序开头——数组赋值:int LEN = 10000; int[][] ...

在拼多多上班,是一种什么样的体验?我心态崩了呀!

之前有很多读者咨询我:武哥,在拼多多上班是一种什么样的体验?由于一直很忙,没抽出时间来和大家分享。上周末特地花点时间来写了一篇文章,跟大家分享一下拼多多的日常。 1. 倒时差的作息 可能很多小伙伴都听说了,拼多多加班很严重。这怎么说呢?作息上确实和其他公司有点区别,大家知道 996,那么自然也就能理解拼多多的“11 11 6”了。 所以当很多小伙伴早上出门时,他们是这样的: 我们是这样的: 当...

相关热词 c# 局部 截图 页面 c#实现简单的文件管理器 c# where c# 取文件夹路径 c# 对比 当天 c# fir 滤波器 c# 和站 队列 c# txt 去空格 c#移除其他类事件 c# 自动截屏
立即提问