TCP UDP 官网教程
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000") //建立连接
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin) //`标准输入`
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
}
}
package main
import (
"bufio"
"fmt"
"net"
)
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000") //监听端口
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据:", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}
这两个案例demo是官网的 主要是演示了** **
客户端发送服务 conn, err := net.Dial("tcp", "127.0.0.1:20000") //建立连接
服务器端的监听端口 listen, err := net.Listen("tcp", "127.0.0.1:20000") //监听端口
思考
- 为什么服务端一定要有
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
//服务器 持续监听 新的连接请求;
每来一个客户端,就启动一个 独立的 goroutine 去处理它;
主循环立即回到 Accept(),准备接收下一个客户端;
实现 并发、多客户端支持。
核心包NET
<font style="color:rgb(6, 10, 38);">Dial(network, address string)</font>:主动连接(客户端使用)<font style="color:rgb(6, 10, 38);">DialTimeout(network, address string, timeout time.Duration)</font>:带超时的连接<font style="color:rgb(6, 10, 38);">Listen(network, address string)</font>:监听端口(服务端使用)<font style="color:rgb(6, 10, 38);">ResolveTCPAddr(network, address string)</font>:解析地址为<font style="color:rgb(6, 10, 38);">*TCPAddr</font><font style="color:rgb(6, 10, 38);">LookupHost(host string)</font>:DNS 查询
UDP案例
package main
import (
"fmt"
"net"
)
// UDP 客户端
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
package main
import (
"fmt"
"net"
)
// UDP/server/main.go
// UDP server端
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0), //监听所有网卡
Port: 30000, //监听端口
}) //创建UDP连接
if err != nil {
fmt.Println("listen failed, err:", err) //监听失败
return
}
defer listen.Close()
for {
var data [1024]byte //创建一个1024字节的缓冲区
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
这次执行会发现跟TCP不一样 你执行一次以后 就会断开连接 这就是UDP UDP 客户端:多数场景是“问一次,答一次”,用完就走 不会建立长连接
小疑问:?UDP既然是用一次回答一次 那么像直播岂不是这种轮询会很多吗 如何保证呢? 直播我看的就是 点进去直播间 就可以一直看了 也没有什么明显的卡顿
A:直播并不是“轮询”,而是“持续单向流式发送”——服务器不停地往客户端发 UDP 包,客户端不停地收,中间没有“问-答”交互。
- 你点进直播间 → 客户端告诉服务器:“我要看这个流”
- 服务器立刻开始 以每秒几十个 UDP 包的速度,源源不断地把视频/音频数据发给你
- 不需要你每帧都“请求”,也不需要你“确认收到”
- 你只是默默接收并播放
这叫 单向实时流(Unidirectional Real-time Stream)
GO websocket入门教程
前情提要:本文章只适合想要入门以后 想要加强网络编程相关概念以及使用的 比小白NB 一点的大白。阅读文章之前 相信你已经知道了 TCP UDP WebSocket 的一些基本的概念 但是对于三者之间的联系 还有到底有什么区别...实战到底有什么不同....如果你是抱着这种心态的话,那么恭喜你,你被我恭喜到了(bushi)这篇一系列教程必然适合你。
WebSokcet Tcp Udp 傻傻分不清
最简单的 TCP 有链接 UDP无连接。 而对于websocket来说
举个例子>> 再我们访问网页的时候 每一个请求其实都是你去通过URL -->发GET/POST请求以后 -->服务器给你返回值 -->然后前端渲染--> 响应 -->你看到。 只是tcp/udp这个样子的 你请求 服务器才会给你,你不请求 服务器不给你 。
然而对于WebSocket来说 服务器说:不管你要不要 我全给你。是的 即使你不请求,只要你连接上了这个WebSocket 那么服务器有什么信息就发给你什么信息。
websocket就像是打电话(确实)
因此 tcp与websocket的区别的关键点就是在于 是否是你要了服务器才给你
其他不同 TCP&WebSocket
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
websocket**握手过程**
客户端 → 服务器: HTTP Upgrade 请求
(带 Sec-WebSocket-Key)
服务器 → 客户端: HTTP 101 Switching Protocols
(返回 Sec-WebSocket-Accept)
连接建立!从此进入 WebSocket 世界
WebSocket 生命周期
握手 → 连接成功 → 持续通信 → 关闭连接
↓ ↓ ↓ ↓
onopen onmessage send() onclose
开始代码
首先要先下载 包
go get -u -v github.com/gorilla/websocket
认识一下新朋友
conn *websocket.Conn
是 Gorilla WebSocket 库中表示一个已建立的 WebSocket 连接的对象。它是你与客户端(比如浏览器)进行双向通信的核心。
核心方法
| 方法 | 作用 |
|---|---|
<font style="color:rgb(6, 10, 38);">ReadMessage() (messageType int, p []byte, err error)</font> | 从客户端读取消息 |
<font style="color:rgb(6, 10, 38);">WriteMessage(messageType int, data []byte) error</font> | 向客户端发送消息 |
<font style="color:rgb(6, 10, 38);">Close() error</font> | 主动关闭连接(发送 Close 帧) |
<font style="color:rgb(6, 10, 38);">SetReadDeadline(t time.Time)</font> / <font style="color:rgb(6, 10, 38);">SetWriteDeadline(t time.Time)</font> | 设置读写超时 |
<font style="color:rgb(6, 10, 38);">LocalAddr()</font> / <font style="color:rgb(6, 10, 38);">RemoteAddr()</font> | 获取本地/客户端网络地址 |
<font style="color:rgb(6, 10, 38);">Subprotocol()</font> | 获取协商的子协议(如 <font style="color:rgb(6, 10, 38);">"chat"</font>) |
写一个echo服务器
Echo 服务器(Echo Server)是一种非常基础的网络服务程序,其核心功能是:将客户端发送过来的数据原样返回(“回显”)给客户端。它常用于网络编程教学、协议测试、连接调试等场景。
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
// 升级 HTTP 连接到 WebSocket函数
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
//todo: 生产环境应该验证来源
return true //这里直接返回true,表示允许所有来源
},
}
// w http.ResponseWriter 是一个接口(interface) 用于向客户端(通常是浏览器或其他 HTTP 客户端)写回响应数据。
// r *http.Request 是一个结构体指针 包含客户端发来的 HTTP 请求的所有信息。
func echo(w http.ResponseWriter, r *http.Request) {
// 1. 升级 HTTP 连接到 WebSocket
conn, err := upgrader.Upgrade(w, r, nil)
// upgrader.Upgrade(w, r, responseHeader http.Header)
/*
responseHeader http.Header 是一个结构体 包含响应头信息
握手响应中添加自定义 HTTP 头。
这个头只在握手阶段发送一次,后续 WebSocket 数据帧不再包含 HTTP 头。
*/
if err != nil {
log.Println("升级失败:", err)
return
}
defer conn.Close()
// 2. 循环读取消息
for {
// 读取客户端消息
messageType, message, err := conn.ReadMessage()
//messageType 有两种类型:文本(TextMessage) 值为 websocket.TextMessage 1
// 和二进制(BinaryMessage) 于发送 任意二进制数据(如图片、音频、Protobuf、自定义字节流) websocket.BinaryMessage 2
if err != nil {
log.Println("读取失败:", err)
break
}
log.Printf("收到: %s", message)
retmsg := []byte("Echo: " + string(message))
// 3. 原样返回(Echo)
err = conn.WriteMessage(messageType, retmsg)
if err != nil {
log.Println("发送失败:", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", echo)
log.Println("服务器启动在 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
HTML(本教程并不交前端 所以我是AI生成的能用就行)
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Echo 测试</title>
</head>
<body>
<h1>WebSocket Echo 测试</h1>
<input id="msg" type="text" placeholder="输入消息">
<button onclick="send()">发送</button>
<div id="messages"></div>
<script>
let socket;
// 连接
function connect() {
socket = new WebSocket('ws://localhost:8080/ws');
socket.onopen = () => {
appendMessage('✅ 连接成功');
};
socket.onmessage = (event) => {
appendMessage('收到: ' + event.data);
};
socket.onclose = () => {
appendMessage('❌ 连接关闭');
};
socket.onerror = (error) => {
appendMessage('❌ 错误: ' + error);
};
}
// 发送消息
function send() {
const msg = document.getElementById('msg').value;
if (msg && socket.readyState === WebSocket.OPEN) {
socket.send(msg);
appendMessage('你: ' + msg);
document.getElementById('msg').value = '';
}
}
// 显示消息
function appendMessage(text) {
const div = document.createElement('div');
div.textContent = text;
document.getElementById('messages').appendChild(div);
}
// 页面加载时连接
window.onload = connect;
</script>
</body>
</html>
或者是写一个client客户端 用来跟服务端建立websocket连接
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 启动一个 goroutine 专门读取服务器消息
go func() {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("读取出错:", err)
return
}
fmt.Printf("← 收到: %s\n", msg)
}
}()
// 主 goroutine 负责从 stdin 读取用户输入并发送
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("请输入消息(输入 'quit' 退出):")
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text == "quit" {
break
}
if err := conn.WriteMessage(websocket.TextMessage, []byte(text)); err != nil {
log.Println("发送失败:", err)
break
}
fmt.Printf("→ 已发送: %s\n", text)
}
}
官方案例demo
框架展示
Server
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
go h.run()
router.HandleFunc("/ws", myws)
if err := http.ListenAndServe("127.0.0.1:8080", router); err != nil {
fmt.Println("err:", err)
}
}
HUB
package main
import "encoding/json"
var h = hub{
c: make(map[*connection]bool),//连接列表
u: make(chan *connection), //注销通道
b: make(chan []byte), //广播通道
r: make(chan *connection),//注册通道
}
type hub struct {
c map[*connection]bool
b chan []byte
r chan *connection
u chan *connection
}
func (h *hub) run() {
for {
select {
case c := <-h.r:
h.c[c] = true
c.data.Ip = c.ws.RemoteAddr().String()
c.data.Type = "handshake"
c.data.UserList = user_list
data_b, _ := json.Marshal(c.data)
c.sc <- data_b
case c := <-h.u:
if _, ok := h.c[c]; ok {
delete(h.c, c)
close(c.sc)
}
case data := <-h.b:
for c := range h.c {
select {
case c.sc <- data:
default:
delete(h.c, c)
close(c.sc)
}
}
}
}
}
DATA
package main
import (
"encoding/json"
"github.com/gorilla/websocket"
)
// Data 是所有 WebSocket 消息的通用结构
type Data struct {
Ip string `json:"ip"` // 客户端 IP
User string `json:"user"` // 当前用户
From string `json:"from"` // 消息来源
Type string `json:"type"` // 消息类型
Content string `json:"content"` // 消息内容
UserList []string `json:"user_list"` // 在线用户列表
}
// 封装发送逻辑
func (d *Data) SendTo(conn *websocket.Conn) error {
data, err := json.Marshal(d)
if err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, data)
}
Connection
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
type connection struct {
ws *websocket.Conn
sc chan []byte
data *Data
}
var user_list = []string{}
var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool {
return true
}}
func myws(w http.ResponseWriter, r *http.Request) {
ws, err := wu.Upgrade(w, r, nil)
if err != nil {
return
}
c := &connection{sc: make(chan []byte, 256), ws: ws, data: &Data{}}
h.r <- c
go c.writer()
c.reader()
defer func() {
c.data.Type = "logout"
user_list = del(user_list, c.data.User)
c.data.UserList = user_list
c.data.Content = c.data.User
data_b, _ := json.Marshal(c.data)
h.b <- data_b
h.r <- c
}()
}
func (c *connection) writer() {
for message := range c.sc {
c.ws.WriteMessage(websocket.TextMessage, message)
}
c.ws.Close()
}
func (c *connection) reader() {
for {
_, message, err := c.ws.ReadMessage()
if err != nil {
h.r <- c
break
}
json.Unmarshal(message, &c.data)
switch c.data.Type {
case "login":
c.data.User = c.data.Content
c.data.From = c.data.User
user_list = append(user_list, c.data.User)
c.data.UserList = user_list
data_b, _ := json.Marshal(c.data)
h.b <- data_b
case "user":
c.data.Type = "user"
data_b, _ := json.Marshal(c.data)
h.b <- data_b
case "logout":
c.data.Type = "logout"
user_list = del(user_list, c.data.User)
data_b, _ := json.Marshal(c.data)
h.b <- data_b
h.r <- c
default:
fmt.Print("========default================")
}
}
}
func del(slice []string, user string) []string {
count := len(slice)
if count == 0 {
return slice
}
if count == 1 && slice[0] == user {
return []string{}
}
var n_slice = []string{}
for i := range slice {
if slice[i] == user && i == count {
return slice[:count]
} else if slice[i] == user {
n_slice = append(slice[:i], slice[i+1:]...)
break
}
}
fmt.Println(n_slice)
return n_slice
}
小问题 Q&A
Q:为什么myws的write 要启一个协程与写 而Read确是要一个无限for循环就可以?
A: 如果write是for循环 就会卡住Read 所以write必须是协程
Q:为什么read不能也是一个协程而是一个for循环
A:但通常 不需要,因为:myws 的主 goroutine 本来就没别的事干,让它负责 reader 更自然减少一个 goroutine,节省资源生命周期更清晰:myws 退出 = 连接关闭
reader() | writer() | |
|---|---|---|
| 是否循环 | ✅必须 for循环 | 也用 for range循环 |
| 是否 goroutine | 不需要(主流程就是它) | ✅必须 go启动 |
| 驱动方式 | 主动从网络读(阻塞 I/O) | 被动从 channel 读(事件驱动) |
| 目的 | 接收客户端指令 | 发送服务端消息给客户端 |
BUT
但是我感觉这个官方的demo有点小问题 字段语义不清晰 而且
c.data.User = c.data.Content
c.data.From = c.data.User 想这一块就很奇怪
所以我写了一个小demo基于官方的demo 更适合新人宝宝的体质
demo升级版(更容易理解)
server
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
// 1. 启动 hub(消息中心)
go h.run()
// 2. 创建路由器
router := mux.NewRouter()
// 3. 注册 WebSocket 路由
router.HandleFunc("/ws", myws)
// 4. 启动 HTTP 服务器
log.Println(" 服务器启动在 http://127.0.0.1:2778")
log.Fatal(http.ListenAndServe("127.0.0.1:2778", router))
}
HUB
package main
import "log"
// hub 是聊天室的核心,管理所有连接和消息广播
type hub struct {
c map[*connection]bool // 所有活跃连接
u chan *connection // 注销连接通道
b chan []byte // 广播消息通道
r chan *connection // 注册新连接通道
}
var h = hub{
c: make(map[*connection]bool),
u: make(chan *connection),
b: make(chan []byte),
r: make(chan *connection),
}
// run 是 hub 的主循环,处理所有事件
func (h *hub) run() {
for {
select {
// 处理新连接注册
case conn := <-h.r:
h.c[conn] = true
log.Printf(" 新连接: %p, 当前连接数: %d", conn, len(h.c))
// 发送握手消息
data := &Data{
Ip: conn.ws.RemoteAddr().String(),
Type: "handshake",
}
data.SendTo(conn.ws)
// 处理连接注销
case conn := <-h.u:
if _, ok := h.c[conn]; ok {
delete(h.c, conn)
close(conn.sc)
log.Printf(" 连接断开: %p, 剩余连接数: %d", conn, len(h.c))
}
// 处理广播消息
case message := <-h.b:
for conn := range h.c {
select {
case conn.sc <- message:
// 发送成功
default:
// 发送失败(channel 满),断开连接
close(conn.sc)
delete(h.c, conn)
}
}
}
}
}
DATA
package main
import (
"encoding/json"
"github.com/gorilla/websocket"
)
// Data 是所有 WebSocket 消息的通用结构
type Data struct {
Ip string `json:"ip"` // 客户端 IP
User string `json:"user"` // 当前用户
From string `json:"from"` // 消息来源
Type string `json:"type"` // 消息类型
Content string `json:"content"` // 消息内容
UserList []string `json:"user_list"` // 在线用户列表
}
// 封装发送逻辑
func (d *Data) SendTo(conn *websocket.Conn) error {
data, err := json.Marshal(d)
if err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, data)
}
CONNECTION
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/websocket"
)
// connection 代表一个客户端连接
type connection struct {
ws *websocket.Conn // 底层 WebSocket 连接
sc chan []byte // 发送消息的 channel
data *Data // 当前处理的消息
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // TODO: 生产环境应验证来源
},
}
// myws 是 WebSocket 的 HTTP 处理函数
func myws(w http.ResponseWriter, r *http.Request) {
// 1. 升级 HTTP 连接到 WebSocket
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("升级失败:", err)
return
}
// 2. 创建 connection
conn := &connection{
ws: ws,
sc: make(chan []byte, 256), // 带缓冲的 channel
}
// 3. 注册到 hub
h.r <- conn
// 4. 启动发送协程
go conn.writer()
// 5. 启动接收循环(阻塞)
conn.reader()
// 6. 退出时注销
h.u <- conn
}
// writer 负责从 sc channel 读取消息并发送
func (c *connection) writer() {
for message := range c.sc {
err := c.ws.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Println("发送失败:", err)
break
}
}
c.ws.Close()
}
// reader 负责读取客户端消息并处理
func (c *connection) reader() {
defer func() {
// 确保退出时关闭连接
c.ws.Close()
}()
for {
// 1. 读取消息
_, message, err := c.ws.ReadMessage()
if err != nil {
log.Println("读取失败:", err)
break
}
// 2. 解析 JSON
var data Data
if err := json.Unmarshal(message, &data); err != nil {
log.Println("解析失败:", err)
continue
}
c.data = &data
// 3. 根据消息类型处理
switch data.Type {
case "login":
handleLogin(c, &data)
case "user":
handleUserMessage(c, &data)
case "logout":
handleLogout(c, &data)
default:
log.Printf("未知消息类型: %s", data.Type)
}
}
}
var userList []string // 全局用户列表(非线程安全!)
// 处理登录
func handleLogin(c *connection, data *Data) {
// 添加用户
userList = append(userList, data.Content)
// 广播新用户上线
broadcastData := &Data{
User: data.Content,
Type: "login",
Content: data.Content + " 加入了聊天室",
UserList: userList,
}
broadcastToAll(broadcastData)
}
// 处理普通消息
func handleUserMessage(c *connection, data *Data) {
// 转发消息给所有人
broadcastData := &Data{
User: data.User,
From: data.From,
Type: "user",
Content: data.Content,
}
broadcastToAll(broadcastData)
}
// 处理登出
func handleLogout(c *connection, data *Data) {
// 从用户列表移除
userList = del(userList, data.Content)
// 广播用户下线
broadcastData := &Data{
User: data.Content,
Type: "logout",
Content: data.Content + " 离开了聊天室",
UserList: userList,
}
broadcastToAll(broadcastData)
}
// 广播给所有连接
func broadcastToAll(data *Data) {
jsonData, _ := json.Marshal(data)
h.b <- jsonData
}
// 从切片中删除元素
func del(slice []string, user string) []string {
for i, u := range slice {
if u == user {
return append(slice[:i], slice[i+1:]...)
}
}
return slice
}