服务之Socket及Websocket介绍

现在我们编写Web应用或者基于RPC应用,其实说白了,都是基于socket的编程。本文就是记录一下自己直接用Go的net包基于底层socket进行实践,编写了服务端和客户端。服务端采用协程,即启动多个goroutine,思想其实和net/http包里面的Server一致:对于每一个新的客户端连接,都会新开一个goroutine,而不影响其他的连接。

上代码吧:

Server:



func handleconn(conn net.Conn) {
	//defer conn.Close()
	fmt.Println(conn.RemoteAddr().String())
	ch := make(chan bool)
	//rch := make(chan bool)
	//读取客户端传送的消息
	go func() {

		fmt.Println("enter go func")
		now := time.Now().String()
		conn.Write([]byte(now))
		fmt.Println("func write complete")
		ch <- true
	}()
	<- ch
	go func() {
		buf := make([]byte, 4)
		n, _ := conn.Read(buf)
		fmt.Println(buf[:n])
		fmt.Println("func read complete")
		ch <- true
	}()
	<- ch
   fmt.Println("read and write finished both")
   }



func main() {
	socket,err := net.Listen("tcp",":8000")
	if err != nil {
		fmt.Println(err)
	}
	for {
		conn,err := socket.Accept()
		if err != nil {
			fmt.Println(err)
			continue
		}
		go handleconn(conn)
	}
}

上述服务端代码已经写了注释,解释了基本设计逻辑。

CLIENT:

package main

import (
	"fmt"
	"net"
	"io/ioutil"
	"os"
)

func CheckError(err error) {
	if err != nil{
		fmt.Println("err:",err)
		os.Exit(1)
	}
}

func main() {
	sockaddr,err := net.ResolveTCPAddr("tcp4","localhost:8000")
	CheckError(err)
   //dial tcp,请求建立和目标服务器的Socket连接。
	conn,err := net.DialTCP("tcp",nil,sockaddr)
	CheckError(err)
    //读取服务器通过已经建立的连接通道发送的数据,读出的是字节流数据
	result,err := ioutil.ReadAll(conn)
    //将byte转换为字符串
	fmt.Println("from server message:",string(result))
	os.Exit(0)
}

基本的业务逻辑还是挺简单的,具体的一些业务逻辑可以在这个基础上进行添加。

补充一点:尽量不要用ioutil.ReadAll,该方法是要等到err或者EOF才会返回结果,即一直阻塞,今天做的可把我坑惨了。可以用buf:

buf := make([]byte,1024)
		n, _ := conn.Read(buf)
		fmt.Println(buf[:n])

Websocket介绍

目前,在socket基础上衍生出了一个WebSocket,其基本思想是每一个Web客户端和服务器建立一个TCP连接,所有的数据通信都走这一个连接通道。

它的出现主要是解决什么问题呢?

我们都知道,对于普通的Web,都是采用HTTP协议,如果我们希望能够实现及时通信,就需要客户端不断地向服务器发送HTTP请求,你有没有新数据,有没有数据,有没有数据。这种经常性地发送HTTP请求势必造成了资源带宽的浪费。而且这也会造成同步的延迟。

上面的问题,通过WebSocket就可以解决。有了Websocket,那客户端和服务端建立了唯一的连接,然后服务端会主动把消息发给服务端。即你建立好之后,客户端就是少女,服务端就是个饥渴男,我有消息,我有消息,我有消息,而不用客户端自己再主动询问。

当然,Websocket同样也需要先进行一次HTTP连接,然后再使用WebSocket进行通信。

在网上找的一个Websocket连接建立的过程示意图:

一个TCP流:

GET /ws/chat/haibo/dhdwdwdnwdindo/ HTTP/1.1
Host: 127.0.0.1:8000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36
Upgrade: websocket
Origin: http://127.0.0.1:8000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: csrftoken=0uzZVr3DkV2JykWkdXJsrTyKb5L5chnLGKD0zIwdHGeH5P2X3x0h2ThnpM0Jkw0O; remember_admin_59ba36addc2b2f9401580f014c7f58ea4e30989d=eyJpdiI6IkRDSDd2SmpcL1lINEgwWUJZclpRd0FBPT0iLCJ2YWx1ZSI6Ik9qVHBOQ21NV1wvamdsVTNaaHo1c05JRUhKNTBVNk0rSXMrMHJ2ZE1iOExESXJpVGM1WDB1OUI4aHlRUkl1VTkrMDUrTHNYMDF5WkY2ZVNURE54YkxvalFsa1ZrbllmaHhxREQrXC9UZU5tcmtiY1R2ak5qVEJSdkFzVzFQRXk4MTdXZHJCVFVlOHAyTnREc0NncDFWTWNXOGFMbFQ1amJrSXluQTlwM1FQRGhZPSIsIm1hYyI6ImE5MTRiNDM1MzUwYzI4YzRjOTNkNDNhM2NiNGE5YTU5MmZhNjNiZTRiY2MzOTQ3YTAwMDkxMzE3MDFjYTAzODgifQ%3D%3D; XSRF-TOKEN=eyJpdiI6Im1YYjZvYTNOaytlaDJyTUlrZitta1E9PSIsInZhbHVlIjoiNldJN0RHRXNjV2hUTjE4KytocGk5UWd5OHdwdjhkQ01XR1wvUWVENHFxMGRaVlRRaDl0aWpIblJDeXpFaVlmSE5xM1FlTm9PUmx3OG1TWEVHUmcweU5BPT0iLCJtYWMiOiI3NjQ1Y2RkOGJmNmYzNzYzNzA4YTBmOWJhOTZhOGEzMjMxZTc1YzgzZjk5Mzg2N2Q4ZDZjYzY5MWNmMDNiNjJiIn0%3D; youpin_team_session=eyJpdiI6IlJOSXdqaUk2bEJFZElqSGcycVRrVkE9PSIsInZhbHVlIjoiRURtYjhIMVoxQkZGcjF6ZW5ZVHRia1JrOWFUcVNnY1Q4K1lFRFZvNTY3UGlENnVBMVFVNzVMdDYyRHhTRk8wbHowZmk1SGJDZVRlVXRMYnA5aVZcL3B3PT0iLCJtYWMiOiJmOTdhYzJlNmE2MTE0MDNjZTVlNTY0NDVjZmUyNDJkNmJiOTNlNmE3NjRjNjczMWNmMWY2MzFmODE2MjQ1NGM3In0%3D
Sec-WebSocket-Key: ++EcxfMKJZv5ZPzm/Ah0+Q==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

HTTP/1.1 101 Switching Protocols
Server: Daphne
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: MDda/nVP5hymfRIxrEqsDeTXFTA=

.......9...h...9...z...f..{"message": "hello\uff1ahaha"}."{"message": "hello\uff1ahahatest"}..4WZj....HB!..(..un7g....;.......z.4SyV....

我用Python和LayIM实现了基本客服功能,主要也借鉴了网上几个不错的项目,综合了几个优秀的,开发了一个。先上图:

用户客服咨询:

实现WebIM,有几个点:

1、前端,websocket和layIM,后端Server的一个交互;

2、后端Websocket Server的处理。

layIM目前并不开源,所以我弄的就是用来学习的,不参与任何的商业。看官网还是比较清晰的。不过,我本身不是前端出身,刚开始也弄了半天。下面是前端实现的一个代码,基本的客服模式。

主要是建立Websocket,并设定每个onMethod方法。

<script>

            //演示代码
    layui.use('layim', function(layim){
          var layim = layui.layim;
          var curuser = $("#barrage").html();
          layim.config({
            init: {
              //配置客户信息
              mine: {
                "username": "访客" //我的昵称
                ,"id": curuser //我的ID
                ,"status": "online" //在线状态 online:在线、hide:隐身
                ,"remark": "在深邃的编码世界,做一枚轻盈的纸飞机" //我的签名
                ,"avatar": "//res.layui.com/images/fly/avatar/00.jpg" //我的头像
              }
            }
            //开启客服模式
            ,brief: true
          });
        //监听发送消息
        layim.on('sendMessage', function (data) {
            //接收消息人员信息
            var To = data.to;
            var mine = data.mine;


            if (To.type === 'friend') {
                layim.setChatStatus('<span style="color:#FF5722;">对方正在输入。。。</span>');
            }
            var msg = JSON.stringify(data);
            //保存消息

            socket.send(msg);

        });

        layim.chat({
              name: '在线客服' //名称
              ,type: 'friend' //聊天类型
              ,avatar: 'http://tp1.sinaimg.cn/5619439268/180/40030060651/1' //头像
              ,id: 9999 //定义唯一的id方便你处理信息
            });

        var socket = null;
    //连接websocket的ip地址
    //动态修改查
        var user =  $("#barrage").html();
        var im = {
            init: function () {
              if ('WebSocket' in window){
               socket = new WebSocket(  'ws://' + window.location.host + '/ws/chat/haibo/' + {{user}} + "/");
                im.startListener();
                }
                else if ('MozWebSocket' in window){
                    //ws = new MozWebSocket("ws://www.isuyu.cn:8086/socketServer/"+$("#username").val());
                    socket = new MozWebSocket('ws://' + window.location.host +  '/ws/chat/haibo/'  + user + "/");
                }
            },
            startListener: function () {
                if (socket) {
                    // 连接发生错误的回调方法
                    socket.onerror = function () {
                        console.log("通讯连接失败!");
                    };
                    // 连接成功建立的回调方法
                    socket.onopen = function (event) {
                        console.log("通讯连接成功");
                    }
                    // 接收到消息的回调方法
                    socket.onmessage = function (event) {
                        console.log("通讯接收到消息");
                        im.handleMessage(event.data);
                    }
                    // 连接关闭的回调方法
                    socket.onclose = function () {
                        console.log("通讯关闭连接!!");
                    }
                }
            },
            handleMessage: function (msg) {
                msg = JSON.parse(msg);
                //如果是群消息,转换一下id,返回群id的的字段必须是id
                if (msg.type == "group") {
                    var temId = msg.id;
                    msg.id = msg.toId;
                    msg.toId = temId;
                }
                //console.log(msg)
              layim.getMessage(msg);
            }
        };
        im.init();
 });

上面代码,仔细看还是比较简单的,没有复杂的逻辑。就是Websocket和layim相互实现函数调用。

接下来就是和后端的交互了。每一种语言都有实现好的框架。因为我的hbnnmall是python构建,所以我就用了Django的channels。springboot也有好用的jar包,都过注解可以实现。其实思想都差不多。

class CustomerService(AsyncWebsocketConsumer):

    async def connect(self):
        self.room_group_name =  self.scope['url_route']['kwargs']['room_name']
        self.token =  self.scope['url_route']['kwargs']['token']
        #TODO 获取uid
        self.uid  = getTempUidByToken(self.token)
        users[ self.uid] = self.channel_name
        print(self.channel_name)
        print(users)
        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )

        await self.accept()
        # await self.sendMessage(self.channel_name, "欢迎访问")
        if self.uid == ADMIN_USER:
            await self.sendMessage(users.get(ADMIN_USER), "欢迎管理员",ADMIN_USER)
            #TODO check所有未被消费的消息,都要发送给管理员
            # MessageProcessor.sendAllUnReadMsg(self.channel_name)
        else:
            await self.sendMessage(users.get(self.uid), "欢迎".format(self.uid))

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    async def receive(self, text_data):
        print(users)
        print("get data:" + text_data)
        text_data_json = json.loads(text_data)
        if "data" in text_data_json:
            mine = text_data_json["data"]["mine"]
        else:
            mine = text_data_json["mine"]
        message = mine['content'] if 'content' in mine else mine['message']
        from_id = int(mine["id"])
        # Send message to room group
        # await self.channel_layer.group_send(
        #     self.room_group_name,
        #     {
        #         'type': 'chat_message',
        #         'message': "您好,{},您的消息:{} 我已收到".format(from_id,message)
        #         ,"toId": ADMIN_USER
        #     }
        # )
        if from_id != ADMIN_USER:
            await self.sendMessage(users.get(from_id), "您好,{},您的消息:{} 我已收到".format(from_id, message), toId=ADMIN_USER)

        #转发给目的地
        if "data" in text_data_json:
            target = text_data_json["data"]["to"]
        else:
            target = text_data_json["to"]
        target_id = int(target["id"])
        if target_id in users:
            print("send msg from {} to {}".format(from_id,target_id))
            await self.sendMessage(users.get(int(target["id"])), message,from_id)
        else:
            #发送消息到消息Redis中,当管理员上线后,消息需要发送给管理员
            await self.sendMessage(users.get(from_id), "对方暂时不在线哦!")


    # Receive message from room group
    async def chat_message(self, event):
        message = event['message']
        print(event.keys())
        uid = event['toId']
        data = {
            'message': message,
            "mine": False,
            'content': message,
            "type": "friend",
             "username": uid,
             "avatar": "http://tp1.sinaimg.cn/1571889140/180/40030060651/1"
            , "id": int(uid)
        , "fromid": int(uid)
        , "timestamp": 1467475443306
        }
        print(data)
        # Send message to WebSocket
        await self.send(text_data=json.dumps(data))


    async def sendMessage(self,channel,msg,toId=None):
        data = {'type': 'chat_message', 'message': msg, "toId": toId if toId is not None else ADMIN_USER}
        print("data:{}".format(data))
        await self.channel_layer.send(
            channel,
           data
        )

    def getUsers(self):
        return users

参考资料:

Websocket实现

看完让你彻底搞懂Websocket原理

Socket与WebSocket

深入浅出Websocket(一)Websocket协议

LayIm

--------EOF---------
微信分享/微信扫码阅读