三、Socket
3.1 什么是Socket
Socket 并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。它可以被看作是一个门面模式,把复杂的TCP/IP协议族 隐藏在Socket接口后面,为上层应用提供了方便的使用方式,对用户来说,一组简单的接口就是全部,底层让Socket去组织数据,以符合指定的协议。
在计算机网络中,传输层和应用层之间的socket(套接字)是一种通信机制,用于在应用程序之间进行数据交流。Socket在应用层和传输控制层之间起到桥梁的作用,将应用层的数据传输需求转化为传输层可以理解和实现的数据传输行为。
具体来说,Socket实际上是一种封装了网络协议(如TCP或UDP)的编程接口,它提供了一组方法和规范,使应用程序能够方便地通过网络进行数据传输。应用层可以利用Socket接口与传输层进行交互,实现 数据在不同应用程序进程或网络连接之间的传输。
Socket偏向于底层,一般很少直接使用Socket来编程,框架底层使用Socket比较多。
使用Socket进行数据传输的过程包括以下步骤:
- 创建Socket:应用程序首先需要创建一个Socket实例,这通常是通过调用Socket类的方法来实现的。
- 绑定地址:Socket实例需要绑定一个网络地址和端口号,以确定数据传输的目标。
- 连接:将Socket连接到目标地址和端口。
- 发送和接收数据:通过Socket接口发送和接收数据。
- 关闭Socket:在数据传输完成后,应用程序需要关闭Socket实例。
通过这种方式,Socket实现了应用层和传输层之间的通信和数据传输,使得不同应用程序进程或网络连接之间可以相互通信和共享数据。
既然linux操作系统中的任何形式的I/O都是对一个文件描述符的读取或写入,那么网络I/O也不例外,通过socket() 函数可以创建网络连接,其返回的socket就是文件描述符,通过socket就可以像操作文件那样来操作网络通信,例如使用read() 函数来读取对端计算机传来的数据,使用write() 函数来向对端计算机发送数据。
3.2 Socket通讯的过程
Socket通信的过程可以大致分为以下几个步骤:
- 创建Socket:在应用程序启动时,根据所需的网络协议(如TCP或UDP)创建一个Socket实例。
- 绑定地址:将Socket实例绑定到一个本地IP地址和端口号,以便能够接收到其他应用程序的连接请求。
- 监听连接:启动Socket实例并开始监听指定的端口号,等待其他应用程序的连接请求。
- 接受连接请求:当另一个应用程序尝试连接到已监听的端口时,Socket实例将接收到连接请求。此时,应用程序可以选择接受或拒绝连接请求。
- 数据传输:如果连接请求被接受,应用程序可以通过Socket实例发送和接收数据。数据传输可以是双向的,即应用程序既可以向其他应用程序发送数据,也可以从其他应用程序接收数据。
- 关闭Socket:当数据传输完成后,应用程序应该关闭对应的Socket实例,以释放相关资源。
需要注意的是,在进行Socket通信时,应该注意数据的传输方式和可靠性,例如可以选择TCP协议以保证数据传输的稳定性和可靠性。同时,也需要注意网络安全问题,如防范网络攻击和数据泄漏等。
在进行网络通信的时候,需要一对socket,一个运行于客户端,一个运行于服务端,下图进行一个简单示意。
- listen-socket,是服务端 用于监听客户端建立连接的socket;
- connect-socket,是客户端 用于连接服务端的socket;
- client-socket,是服务端 监听到客户端连接请求后,在服务端生成的与客户端连接的socket。
那么整个通信流程可以进行如下概括:
- 服务端运行后,会在服务端创建listen-socket,listen-socket会绑定服务端的ip和port,然后服务端进入监听状态;
- 客户端请求服务端时,客户端创建connect-socket,connect-socket描述了其要连接的服务端的listen-socket,然后connect-socket向listen-socket发起连接请求;
- connect-socket与listen-socket成功连接后(TCP三次握手成功),服务端会为已连接的客户端创建一个代表该客户端的client-socket,用于后续和客户端进行通信;
- 客户端与服务端通过socket进行网络I/O操作,此时就实现了客户端和服务端中的不同进程的通信。
3.3 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]:%sn", 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("发送的内容:%sn", string(buf))
//发送数据
conn.Write(buf)
//阻塞等待服务器回复的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到据[%d]:%sn", n, string(result))
}
}
3.4 说说 Websocket 与 Socket 的区别
首先说下他们之间的关系:比起捕风捉影的告诉你他们之间的那一丁点联系,我更宁愿告诉你,他们之间几乎毫无关系,就跟雷锋和雷峰塔,周杰和周杰伦一样,只是叫法相似。
- WebSocket是基于TCP的应用层协议,用来解决HTTP 不支持持久化连接的问题。
- Socket是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口,把复杂的TCP/IP协议族 隐藏在Socket接口后面,为上层应用提供了方便的使用方式
3.5.1 websocket是什么
首先,websocket本质上是基于TCP协议的应用层协议,它是伴随 H5 而出的协议,用来解决HTTP 不支持持久化连接的问题。
跟HTTP类似,但两者应用场景不同。
平时我们打开网页,比如购物网站某宝。都是点一下列表商品,跳转一下网页就到了商品详情。从HTTP协议的角度来看,就是点一下网页上的某个按钮,前端发一次HTTP请求,网站返回一次HTTP响应。这种由客户端主动请求,服务器响应的方式也满足大部分网页的功能场景。
但有没有发现,这种情况下,服务器从来就不会主动给客户端发一次消息。
但如果现在,你在刷网页的时候右下角突然弹出一个小广告,提示你【一个人在家偷偷才能玩哦】你点开后发现。长相平平无奇的古某提示你"道士9条狗,全服横着走"。来都来了,你就选了个角色进到了游戏界面里。这时候,上来就是一个小怪,从远处走来,然后疯狂拿木棒子抽你。
你全程没点任何一次鼠标。服务器就自动将怪物的移动数据和攻击数据源源不断发给你了,像这种看起来服务器主动发消息给客户端的场景,是怎么做到的?
这就得说下webSocket了。
我们知道TCP连接的两端,同一时间里,双方都可以主动向对方发送数据。这就是所谓的全双工。
而现在使用最广泛的HTTP1.1,也是基于TCP协议的,同一时间里,客户端和服务器只能有一方主动发数据,这就是所谓的半双工。
也就是说,好好的全双工TCP,被HTTP用成了半双工,为什么?
这是由于HTTP协议设计之初,考虑的是看看网页文本的场景,能做到客户端发起请求 再由 服务器响应,就够了,根本就没考虑网页游戏这种,客户端和服务器之间都要互相主动发大量数据的场景。
所以为了更好的支持这样的场景,我们需要另外一个基于TCP的新协议,于是新的应用层协议websocket就被设计出来了。
大家别被这个名字给带偏了。虽然名字带了个socket,但其实socket和websocket之间,就跟雷峰和雷峰塔一样,二者接近毫无关系。
3.5.2 websocket的使用场景
websocket完美继承了TCP协议的全双工能力,并且还贴心的提供了解决粘包的方案。它适用于需要服务器和客户端(浏览器)频繁交互的大部分场景。比如网页/小程序游戏,网页聊天室,以及一些类似飞书这样的网页协同办公软件。
3.5.3 代码示例
代码语言:javascript复制 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) {}
3.5.4 websocket 与 socket
- socket:
- Socket其实并不是一个协议,而是为了方便使用TCP或UDP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。当两台主机通信时,必须通过Socket连接,Socket则利用TCP/IP协议建立TCP连接。
- Socket 其实就是等于IP 地址 端口 协议。
- 具体来说,Socket 是一套标准,它完成了对 TCP/IP 的高度封装,屏蔽网络细节,以方便开发者更好地进行网络编程。
- websocket:
- WebSocket则是一个典型的应用层协议。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!