Socket 并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。它可以被看作是一个门面模式,把复杂的TCP/IP协议族 隐藏在Socket接口后面,为上层应用提供了方便的使用方式,对用户来说,一组简单的接口就是全部,底层让Socket去组织数据,以符合指定的协议。
在计算机网络中,传输层和应用层之间的socket(套接字)是一种通信机制,用于在应用程序之间进行数据交流。Socket在应用层和传输控制层之间起到桥梁的作用,将应用层的数据传输需求转化为传输层可以理解和实现的数据传输行为。
具体来说,Socket实际上是一种封装了网络协议(如TCP或UDP)的编程接口,它提供了一组方法和规范,使应用程序能够方便地通过网络进行数据传输。应用层可以利用Socket接口与传输层进行交互,实现 数据在不同应用程序进程或网络连接之间的传输。
Socket偏向于底层,一般很少直接使用Socket来编程,框架底层使用Socket比较多。
使用Socket进行数据传输的过程包括以下步骤:
通过这种方式,Socket实现了应用层和传输层之间的通信和数据传输,使得不同应用程序进程或网络连接之间可以相互通信和共享数据。
既然linux操作系统中的任何形式的I/O都是对一个文件描述符的读取或写入,那么网络I/O也不例外,通过socket() 函数可以创建网络连接,其返回的socket就是文件描述符,通过socket就可以像操作文件那样来操作网络通信,例如使用read() 函数来读取对端计算机传来的数据,使用write() 函数来向对端计算机发送数据。
Socket通信的过程可以大致分为以下几个步骤:
需要注意的是,在进行Socket通信时,应该注意数据的传输方式和可靠性,例如可以选择TCP协议以保证数据传输的稳定性和可靠性。同时,也需要注意网络安全问题,如防范网络攻击和数据泄漏等。
在进行网络通信的时候,需要一对socket,一个运行于客户端,一个运行于服务端,下图进行一个简单示意。
那么整个通信流程可以进行如下概括:
package main
import (
"fmt"
"log"
"net"
"strings"
)
func dealConn(conn net.Conn) {
defer conn.Close() //此函数结束时,关闭Socket
//conn.RemoteAddr().String():连接客服端的网络地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "连接成功")
buf := make([]byte, 1024) //缓冲区,用于接收客户端发送的数据
// 阻塞等待用户发送的数据
for {
n, err := conn.Read(buf) //n接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据来自[%s]==>[%d]:%s\n", ipAddr, n, string(result))
if "exit" == string(result) { //如果对方发送"exit",退出此链接
fmt.Println(ipAddr, "退出连接")
return
}
//把接收到的数据转换为大写,再给客户端发送
conn.Write([]byte(strings.ToUpper(string(result))))
}
}
func main() {
//创建、监听socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
}
defer listenner.Close()
for {
conn, err := listenner.Accept() //阻塞等待客户端连接
if err != nil {
log.Println(err)
continue
}
go dealConn(conn)
}
}
package main
import (
"fmt"
"log"
"net"
)
func main() {
//客户端主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
return
}
defer conn.Close() //关闭
buf := make([]byte, 1024) //缓冲区
for {
fmt.Printf("请输入发送的内容:")
fmt.Scan(&buf)
fmt.Printf("发送的内容:%s\n", string(buf))
//发送数据
conn.Write(buf)
//阻塞等待服务器回复的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到据[%d]:%s\n", n, string(result))
}
}
首先说下他们之间的关系:比起捕风捉影的告诉你他们之间的那一丁点联系,我更宁愿告诉你,他们之间几乎毫无关系,就跟雷锋和雷峰塔,周杰和周杰伦一样,只是叫法相似。
首先,websocket本质上是基于TCP协议的应用层协议,它是伴随 H5 而出的协议,用来解决HTTP 不支持持久化连接的问题。
跟HTTP类似,但两者应用场景不同。
平时我们打开网页,比如购物网站某宝。都是点一下列表商品,跳转一下网页就到了商品详情。从HTTP协议的角度来看,就是点一下网页上的某个按钮,前端发一次HTTP请求,网站返回一次HTTP响应。这种由客户端主动请求,服务器响应的方式也满足大部分网页的功能场景。
但有没有发现,这种情况下,服务器从来就不会主动给客户端发一次消息。
但如果现在,你在刷网页的时候右下角突然弹出一个小广告,提示你【一个人在家偷偷才能玩哦】你点开后发现。长相平平无奇的古某提示你"道士9条狗,全服横着走"。来都来了,你就选了个角色进到了游戏界面里。这时候,上来就是一个小怪,从远处走来,然后疯狂拿木棒子抽你。
你全程没点任何一次鼠标。服务器就自动将怪物的移动数据和攻击数据源源不断发给你了,像这种看起来服务器主动发消息给客户端的场景,是怎么做到的?
这就得说下webSocket了。
我们知道TCP连接的两端,同一时间里,双方都可以主动向对方发送数据。这就是所谓的全双工。
而现在使用最广泛的HTTP1.1,也是基于TCP协议的,同一时间里,客户端和服务器只能有一方主动发数据,这就是所谓的半双工。
也就是说,好好的全双工TCP,被HTTP用成了半双工,为什么?
这是由于HTTP协议设计之初,考虑的是看看网页文本的场景,能做到客户端发起请求 再由 服务器响应,就够了,根本就没考虑网页游戏这种,客户端和服务器之间都要互相主动发大量数据的场景。
所以为了更好的支持这样的场景,我们需要另外一个基于TCP的新协议,于是新的应用层协议websocket就被设计出来了。
大家别被这个名字给带偏了。虽然名字带了个socket,但其实socket和websocket之间,就跟雷峰和雷峰塔一样,二者接近毫无关系。
websocket完美继承了TCP协议的全双工能力,并且还贴心的提供了解决粘包的方案。它适用于需要服务器和客户端(浏览器)频繁交互的大部分场景。比如网页/小程序游戏,网页聊天室,以及一些类似飞书这样的网页协同办公软件。
package chat_api
import (
"encoding/json"
"fmt"
"gin-vue-blog/global"
"gin-vue-blog/models"
"gin-vue-blog/models/ctype"
"gin-vue-blog/utils"
"gin-vue-blog/utils/resp"
"github.com/DanPlayer/randomname"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"net/http"
"path"
"strings"
"time"
)
// ChatGroupRequest 入参
type ChatGroupRequest struct {
MsgContent string `json:"msg_content"`
MsgType ctype.MsgType `json:"msg_type"`
}
// ChatGroupResponse 出参
type ChatGroupResponse struct {
NickName string `json:"nick_name"`
Avatar string `json:"avatar"`
MsgContent string `json:"msg_content"`
MsgType ctype.MsgType `json:"msg_type"`
Date string `json:"date"`
OnlineCount int `json:"online_count"`
}
type ChatUser struct {
Conn *websocket.Conn // 客户端连接
NickName string `json:"nick_name"`
Avatar string `json:"avatar"`
}
var connGroup = make(map[string]ChatUser)
// ChatGroupView 群聊
func (ChatApi) ChatGroupView(c *gin.Context) {
// 1. 定义一个 websocker.Upgrader 结构。
// 这将保存诸如 WebSocket 连接的读取和写入缓冲区大小之类的信息
upGrader := websocket.Upgrader{}
// 2. 检查传入来源
// 确定是否允许来自不同域的传入请求连接,如果不是,它们将被CORS错误击中。
upGrader.CheckOrigin = func(r *http.Request) bool { return true }
// 3. 升级的HTTP连接
conn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
resp.FailWithMsg(c, "群聊功能启动失败")
global.Log.Error(err)
return
}
// 随机生成昵称,根据昵称第一个字关联头像地址
nickName := randomname.GenerateName()
avatarPath := path.Join("/uploads/chat_avatar", string([]rune(nickName)[0])+".png")
addr := conn.RemoteAddr().String()
chatUser := ChatUser{
Conn: conn,
NickName: nickName,
Avatar: avatarPath,
}
connGroup[addr] = chatUser // 记录客户端
// 4. 持续聆听该连接
for {
// 4.1 监听消息
_, messageBytes, err := conn.ReadMessage()
if err != nil { // 用户断开连接
SendMsgToAllClint(addr, ChatGroupResponse{
NickName: chatUser.NickName,
Avatar: chatUser.Avatar,
MsgContent: fmt.Sprintf("[%s]退出聊天室", chatUser.NickName),
MsgType: ctype.OutRoomMsg,
Date: time.Now().Format("2006-01-02 15:04:05"),
OnlineCount: len(connGroup) - 1,
})
break
}
// 4.2 消息参数绑定
var chatGroupRequest ChatGroupRequest
err = json.Unmarshal(messageBytes, &chatGroupRequest)
if err != nil { // 参数绑定失败,给这个客户端反馈即可
SendMsgToOneClint(addr, ChatGroupResponse{
NickName: chatUser.NickName,
Avatar: chatUser.Avatar,
MsgType: ctype.SystemMsg,
MsgContent: "消息参数绑定失败!",
OnlineCount: len(connGroup),
})
continue
}
// 4.3 判断类型,根据不同类型,做不同分发逻辑
switch chatGroupRequest.MsgType {
case ctype.TextMsg: // 普通文本类型
if strings.TrimSpace(chatGroupRequest.MsgContent) == "" {
SendMsgToOneClint(addr, ChatGroupResponse{
NickName: chatUser.NickName,
Avatar: chatUser.Avatar,
MsgType: ctype.SystemMsg,
MsgContent: "消息内容不能为空",
OnlineCount: len(connGroup),
})
continue
}
SendMsgToAllClint(addr, ChatGroupResponse{
NickName: chatUser.NickName,
Avatar: chatUser.Avatar,
MsgContent: chatGroupRequest.MsgContent,
MsgType: ctype.TextMsg,
Date: time.Now().Format("2006-01-02 15:04:05"),
OnlineCount: len(connGroup),
})
case ctype.InRoomMsg: // 进入聊天室通知;会有一个进入聊天室按钮,点击之后,前端发送InRoomMsg过来
SendMsgToAllClint(addr, ChatGroupResponse{
NickName: chatUser.NickName,
Avatar: chatUser.Avatar,
MsgContent: fmt.Sprintf("[%s] 进入聊天室", chatUser.NickName),
MsgType: ctype.InRoomMsg,
Date: time.Now().Format("2006-01-02 15:04:05"),
OnlineCount: len(connGroup),
})
default:
SendMsgToOneClint(addr, ChatGroupResponse{ // 系统反馈
NickName: chatUser.NickName,
Avatar: chatUser.Avatar,
MsgType: ctype.SystemMsg,
MsgContent: "消息类型错误",
OnlineCount: len(connGroup),
})
}
}
defer func(conn *websocket.Conn) {
err := conn.Close()
if err != nil {
global.Log.Errorf("websocket关闭失败. Error: %s", err)
return
}
}(conn)
delete(connGroup, addr)
}
// SendMsgToAllClint 发送信息到所有客户端
// TODO: 这里暂且写为文本类型,后续可以完善图片、视频、语音的发送,对应的就是二进制类型
func SendMsgToAllClint(senderAddr string, chatGroupResponse ChatGroupResponse) {
...
byteData, _ := json.Marshal(chatGroupResponse)
for _, chatUser := range connGroup {
_ = chatUser.Conn.WriteMessage(websocket.TextMessage, byteData)
}
}
// SendMsgToOneClint 系统消息,发送给指定客户端
// TODO: 这里暂且写为文本类型,后续可以完善图片、视频、语音的发送,对应的就是二进制类型
func SendMsgToOneClint(receiverAddr string, response ChatGroupResponse) {
...
bytes, _ := json.Marshal(response)
chatUser := connGroup[receiverAddr]
_ = chatUser.Conn.WriteMessage(websocket.TextMessage, bytes)
}
// 获取用户 IP 和 地址
func getIpAndUserAddr(addr string) (string, string) {}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有