golang实现http静态文件服务

这个例子只是一个非常简单的学习示例,用来说明用socket编程来实现一个http服务的例子,大家可以举一反三。

package main

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

func FileGetContents(fileName string) (string, error) {
	file, err := os.Open(fileName)
	if err != nil {
		return "", err
	}
	defer file.Close()
	content, err := ioutil.ReadAll(file)
	return string(content), nil
}

func responseHtml(fileName string) string {
	body, _ := FileGetContents(fileName)
	bodyLen := len(body)
	result := ""
	result += "HTTP/1.1 200\r\n"
	result += "Content-Length:" + strconv.Itoa(bodyLen) + "\r\n"
	result += "Content-Type: text/html;charset=utf-8\r\n"
	result += "\r\n" + body + "\r\n"
	return result
}

func readLine(bufr *bufio.Reader) ([]byte, error) {
	p, isPrefix, err := bufr.ReadLine()
	if err != nil {
		return p, err
	}
	var l []byte
	for isPrefix {
		l, isPrefix, err = bufr.ReadLine()
		if err != nil {
			break
		}
		p = append(p, l...)
	}
	return p, err
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		line, err := readLine(reader)
		// 数据读取正确
		if err == nil {
			requestMethod := ""
			requestUrl := ""
			requestProto := ""
			fmt.Sscanf(string(line), "%s%s%s", &requestMethod, &requestUrl, &requestProto)
			fmt.Println("request:", requestMethod, requestUrl, requestProto)
			// 回复消息
			conn.Write([]byte(responseHtml("." + requestUrl)))
		} else {
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

发表在 好文推荐 | 标签为 , | 2 条评论

socket分享目录

主要按以下顺序分享

  1. golang的socket的hello world
  2. golang的socket的粘包拆包问题
  3. golang的socket的群聊
  4. golang的socket的单聊
  5. golang的socket的安全(连接安全)
  6. golang的socket的安全(传输安全)
  7. golang的socket的http的api
  8. golang的socket的分布式部署
  9. 一个聊天服务实现的要点
  10. Centrifugo项目的使用分享
  11. golang实现http静态文件服务
  12. golang简单实现redis服务端

发表在 好文推荐 | 标签为 | 一条评论

一个聊天服务实现的要点

综合以上文章,我们基本可以实现一个高性能的即时通信服务了,但要想完整实事一个可用的聊天的服务,还需要考虑以下细节,大家可以一起探讨下

  1. 多端登录
  2. 在线状态
  3. 发消息安全/收消息安全
  4. 重发攻击
  5. 心跳
  6. 群组
  7. 消息保存
  8. 未读消息,主要是每个用户对每个人或者群的小红点提示
  9. 消息重复/消息重试
  10. 消息已读
  11. 一般架构是怎么样的


欢迎大家在此评论探讨!!!!



发表在 好文推荐 | 标签为 | 留下评论

golang的socket的http的api

一般做为一个聊天服务,经常有需求第三方发送消息的场景,如果能够提供一个http的api的写消息服务会方便很多,现在我们来简单实现下。

package main

/*
* socket服务+http服务
 */

import (
	"bufio"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
)

//保存连接
var connList sync.Map

//消息结构{"from":"aaa","to":"bbb","body":"aaa","cmd":"sendMsg"}
type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(str), &strStruct)
			if err != nil {
				fmt.Printf("msg json error, err:%v\n", err)
			} else {
				to := strStruct.To
				cmd := strStruct.Cmd
				if cmd == "sendMsg" {
					toUser, ok := connList.Load(to)
					if !ok {
						fmt.Println("to user is not\n")
					} else {
						toUserConn, ok := toUser.(net.Conn)
						if !ok {
							fmt.Println("to user type wrong\n")
						} else {
							toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
						}
					}
				} else {
					fmt.Println("unkonw msg")
				}
			}
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func socketServer() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

func main() {

	//socket服务
	go socketServer()

	r := gin.Default()

	r.GET("/sendOne", func(c *gin.Context) {
		to := c.DefaultQuery("to", "")
		msg := c.DefaultQuery("msg", "")
		toUser, ok := connList.Load(to)
		if !ok {
			c.String(http.StatusOK, "error1")
		} else {
			toUserConn, ok := toUser.(net.Conn)
			if !ok {
				c.String(http.StatusOK, "error2")
			} else {
				toUserConn.Write([]byte(msg + "\r\n"))
				c.String(http.StatusOK, "ok")
			}
		}
	})

	r.GET("/sendAll", func(c *gin.Context) {
		msg := c.DefaultQuery("msg", "")
		//广播消息
		connList.Range(func(k, userConn interface{}) bool {
			userConn.(net.Conn).Write([]byte(msg + "\r\n"))
			return true
		})
		c.String(http.StatusOK, "ok")
	})

	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

这样我们就简单实现了一个可以通过http的api来发消息的服务,是不是感觉很方便呢?

发表在 好文推荐 | 标签为 | 留下评论

golang的socket的安全(传输安全)

前面的实现完全是在网络上裸奔的,通过抓包工具很容易看到传输的内容是什么,接下来我们利用现成的TLS来实现一个更为安全的服务.

生成服务器端的私钥

openssl genrsa -out server.key 2048

生成服务器端证书

openssl req -new -x509 -key server.key -out server.pem -days 3650

服务端代码如下

package main

/*
* tls服务端
 */

import (
	"bufio"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

//消息结构{"from":"aaa","to":"bbb","body":"aaa","cmd":"sendMsg"}
type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	conn.Write([]byte("hello\n"))
	//安全认证+读取超时
	conn.SetReadDeadline(time.Now().Add(20 * time.Second))
	reader := bufio.NewReader(conn)
	str, err := reader.ReadString('\n')
	if err == nil {
		str = strings.TrimSpace(str)
		var strStruct UserMsg
		err := json.Unmarshal([]byte(str), &strStruct)
		if err != nil {
			fmt.Printf("msg json error, err:%v\n", err)
			return
		} else {
			if strStruct.Cmd == "auth" {
				if strStruct.Body != "123456" {
					fmt.Println("not auth")
					return
				}
			} else {
				fmt.Println("not auth")
				return
			}
		}
	} else {
		return
	}

	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))

	// 针对当前连接做发送和接受操作
	for {
		conn.SetReadDeadline(time.Now().Add(3600 * time.Second))
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(str), &strStruct)
			if err != nil {
				fmt.Printf("msg json error, err:%v\n", err)
			} else {
				to := strStruct.To
				cmd := strStruct.Cmd
				if cmd == "sendMsg" {
					toUser, ok := connList.Load(to)
					if !ok {
						fmt.Println("to user is not\n")
					} else {
						toUserConn, ok := toUser.(net.Conn)
						if !ok {
							fmt.Println("to user type wrong\n")
						} else {
							toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
						}
					}
				} else {
					fmt.Println("unkonw msg")
				}
			}
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	cert, err := tls.LoadX509KeyPair("server.pem", "server.key")
	if err != nil {
		fmt.Println(err)
		return
	}
	config := &tls.Config{Certificates: []tls.Certificate{cert}}
	// 建立tcp 服务
	listen, err := tls.Listen("tcp", "0.0.0.0:1215", config)
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

生成客户端的私钥

openssl genrsa -out client.key 2048

生成客户端的证书

openssl req -new -x509 -key client.key -out client.pem -days 3650

客户端的代码如下:

package main

import (
	"crypto/tls"
	"log"
	"time"
)

func main() {
	conf := &tls.Config{
		InsecureSkipVerify: true,
	}
	conn, err := tls.Dial("tcp", "127.0.0.1:1215", conf)
	if err != nil {
		log.Println(err)
		return
	}
	defer conn.Close()
	conn.Write([]byte("{\"from\":\"aaa\",\"to\":\"678292\",\"body\":\"123456\",\"cmd\":\"auth\"}\n"))
	conn.Write([]byte("{\"from\":\"aaa\",\"to\":\"678292\",\"body\":\"123456\",\"cmd\":\"sendMsg\"}\n"))
	buf := make([]byte, 100)
	n, err := conn.Read(buf)
	if err != nil {
		log.Println(n, err)
		return
	}
	println(string(buf[:n]))
	time.Sleep(time.Second * 10)
}

通过使用tls我们就快速实现了一个更为安全的socket服务.

发表在 网站开发 | 标签为 | 一条评论

golang的socket的安全(连接安全)

对于一个socket服务,没有任何安全措施肯定是不对的,这样就会造成任何人都可以来连接,进而造成不可预知的风险。现在我们来实现一个有安全认证的一个服务.

package main

/*
* 安全认证1,只实现了认证,仍存在风险
 */

import (
	"bufio"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

//消息结构{"from":"aaa","to":"bbb","body":"aaa","cmd":"sendMsg"}
type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	conn.Write([]byte("hello\n"))
	//安全认证
	reader := bufio.NewReader(conn)
	str, err := reader.ReadString('\n')
	if err == nil {
		str = strings.TrimSpace(str)
		var strStruct UserMsg
		err := json.Unmarshal([]byte(str), &strStruct)
		if err != nil {
			fmt.Printf("msg json error, err:%v\n", err)
			return
		} else {
			if strStruct.Cmd == "auth" {
				if strStruct.Body != "123456" {
					fmt.Println("not auth")
					return
				}
			} else {
				fmt.Println("not auth")
				return
			}
		}
	} else {
		return
	}

	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))

	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(str), &strStruct)
			if err != nil {
				fmt.Printf("msg json error, err:%v\n", err)
			} else {
				to := strStruct.To
				cmd := strStruct.Cmd
				if cmd == "sendMsg" {
					toUser, ok := connList.Load(to)
					if !ok {
						fmt.Println("to user is not\n")
					} else {
						toUserConn, ok := toUser.(net.Conn)
						if !ok {
							fmt.Println("to user type wrong\n")
						} else {
							toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
						}
					}
				} else {
					fmt.Println("unkonw msg")
				}
			}
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

本代码基本实现了一个连接认证的逻辑,没有认证的连接是无法进行操作的,但是仍然存在一定的安全隐患,如果一个连接来了之后不进行认证,一直呆在那里,如果这样的空闲连接特别的多,也会造成服务资源的消耗和浪费,接下来我们看看怎么进行优化

package main

/*
* 安全认证2,实现了认证加超时
 */

import (
	"bufio"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

//消息结构{"from":"aaa","to":"bbb","body":"aaa","cmd":"sendMsg"}
type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	conn.Write([]byte("hello\n"))
	//安全认证+读取超时
	conn.SetReadDeadline(time.Now().Add(2 * time.Second))
	reader := bufio.NewReader(conn)
	str, err := reader.ReadString('\n')
	if err == nil {
		str = strings.TrimSpace(str)
		var strStruct UserMsg
		err := json.Unmarshal([]byte(str), &strStruct)
		if err != nil {
			fmt.Printf("msg json error, err:%v\n", err)
			return
		} else {
			if strStruct.Cmd == "auth" {
				if strStruct.Body != "123456" {
					fmt.Println("not auth")
					return
				}
			} else {
				fmt.Println("not auth")
				return
			}
		}
	} else {
		return
	}

	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))

	// 针对当前连接做发送和接受操作
	for {
		conn.SetReadDeadline(time.Now().Add(3600 * time.Second))
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(str), &strStruct)
			if err != nil {
				fmt.Printf("msg json error, err:%v\n", err)
			} else {
				to := strStruct.To
				cmd := strStruct.Cmd
				if cmd == "sendMsg" {
					toUser, ok := connList.Load(to)
					if !ok {
						fmt.Println("to user is not\n")
					} else {
						toUserConn, ok := toUser.(net.Conn)
						if !ok {
							fmt.Println("to user type wrong\n")
						} else {
							toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
						}
					}
				} else {
					fmt.Println("unkonw msg")
				}
			}
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

以上主要的逻辑就是在认证读取的时候增加了超时进而主动关闭连接,防止空闲连接太多.

发表在 好文推荐 | 标签为 , | 3 条评论

golang的socket的分布式部署

随着业务的发展,线上部署肯定不会是一台服务器,当多个服务器的时候,前面的程序是无法满足的,当一个用户连接到主机A,而另一个用户连接到主机B,这两个用户又如何通信呢?

本篇我们来解决这个问题,我们通过在生成的连接标识到带有相关信息和转发机制来实现,代码如下:

package main

/*
* 支持分布式
 */

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

var Ip, Port string

//消息结构{"from":"aaa","to":"bbb","body":"aaa","cmd":"sendMsg"}
type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return Ip + ":" + Port + ":" + string(b)
}

//得到本机IP
func getLocalIpV4() string {
	inters, err := net.Interfaces()
	if err != nil {
		panic(err)
	}
	for _, inter := range inters {
		// 判断网卡是否开启,过滤本地环回接口
		if inter.Flags&net.FlagUp != 0 && !strings.HasPrefix(inter.Name, "lo") {
			// 获取网卡下所有的地址
			addrs, err := inter.Addrs()
			if err != nil {
				continue
			}
			for _, addr := range addrs {
				if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
					//判断是否存在IPV4 IP 如果没有过滤
					if ipnet.IP.To4() != nil {
						return ipnet.IP.String()
					}
				}
			}
		}
	}
	return ""
}

//转发消息
func forwardMsg(ip, port, msg string) {
	conn, _ := net.Dial("tcp", ip+":"+port)
	defer conn.Close()
	conn.Write([]byte(msg))
	time.Sleep(time.Second)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(str), &strStruct)
			if err != nil {
				fmt.Printf("msg json error, err:%v\n", err)
			} else {
				to := strStruct.To
				cmd := strStruct.Cmd
				if cmd == "sendMsg" {
					toArr := strings.Split(to, ":")
					if toArr[0] == Ip && toArr[1] == Port {
						toUser, ok := connList.Load(to)
						if !ok {
							fmt.Println("to user is not\n")
						} else {
							toUserConn, ok := toUser.(net.Conn)
							if !ok {
								fmt.Println("to user type wrong\n")
							} else {
								toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
							}
						}
					} else {
						forwardMsg(toArr[0], toArr[1], str+"\n")
						fmt.Println("forwardMsg " + toArr[0] + toArr[1] + str)
					}

				} else {
					fmt.Println("unkonw msg")
				}
			}
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	flag.StringVar(&Port, "Port", "1215", "端口")
	flag.Parse()
	Ip = getLocalIpV4()
	// 建立tcp 服务
	listen, err := net.Listen("tcp", Ip+":"+Port)
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}
	fmt.Println("listen " + Ip + ":" + Port)
	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

这是服务端代码,我们发消息的格式如下:

{"from":"127.0.0.1:1216:123456","to":"127.0.0.1:1215:651444","body":"1234567","cmd":"sendMsg"}

这样我们就可能实现一个可以分布属部署的程序了,不过还有一种简单且使用较多的分布式方式,那就是利用redis的订阅发布机制,客户端通过socket服务来订阅redis来实现消息的收发,这里不做详细介绍,感兴趣的同学可以尝试做一下!

发表在 好文推荐 | 标签为 | 一条评论

golang的socket的单聊

接下来我们来演示如何实现单聊,既然群聊可以,那么单聊就是在于如何区分用户,然后找到用户发送消息即可.

package main

/*
* 单聊--一对一聊天
 */

import (
	"bufio"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

//消息结构{"from":"aaa","to":"bbb","body":"aaa","cmd":"sendMsg"}
type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(str), &strStruct)
			if err != nil {
				fmt.Printf("msg json error, err:%v\n", err)
			} else {
				to := strStruct.To
				cmd := strStruct.Cmd
				if cmd == "sendMsg" {
					toUser, ok := connList.Load(to)
					if !ok {
						fmt.Println("to user is not\n")
					} else {
						toUserConn, ok := toUser.(net.Conn)
						if !ok {
							fmt.Println("to user type wrong\n")
						} else {
							toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
						}
					}
				} else {
					fmt.Println("unkonw msg")
				}
			}
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

这个例子是通过json协议来标识消息来源于那里,要发送给谁,消息类型是什么,消息内容是什么来实现单聊的.

发表在 好文推荐 | 标签为 | 留下评论

golang的socket的群聊

在前面2篇的文章,我们一直说的是一个客户端和一个服务的端的交互模式,现在我们来说下群聊模式,即多个客户端和一个服务端的交互模式,关键的知识点在于服务端如何转发消息。

我们现在通过一个map来存储所有的连接,然后通过遍历map来对所有连接发消息,代码如下:

package main

/*
* 群聊--广播消息
 */

import (
	"bufio"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			//广播消息
			connList.Range(func(k, userConn interface{}) bool {
				userConn.(net.Conn).Write([]byte(" say : " + str + "\r\n"))
				return true
			})
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

启动上面的代码做服务端,然后通过启动多个telnet,就可以发现,每个telnet端发的消息,其他telnet也可以收到.

发表在 好文推荐 | 标签为 | 一条评论

golang的socket的粘包拆包问题

接下来我们来学习另一个知识点,粘包和拆包,在上一篇文章中,我们采用了”reader.ReadString(‘\n’)”来简单处理粘包和拆包问题,现在我们来深入学习一下。

服务端代码示例:

package main

/*
* 粘包和拆包问题
 */

import (
	"fmt"
	"net"
	"time"
)

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	var buf = make([]byte, 5)
	// 针对当前连接做发送和接受操作
	for {
		n, err := conn.Read(buf)
		if err != nil {
			break
		}
		if n > 0 {
			fmt.Println("received msg", n, "bytes:", string(buf[:n]))
			//模拟处理消息耗时
			time.Sleep(time.Second)
		}

	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

客户端发送消息代码示例:

package main

import (
	"net"
	"time"
)

func main() {
	conn, _ := net.Dial("tcp", "0.0.0.0:1215")
	defer conn.Close()
	for i := 0; i < 10; i++ {
		conn.Write([]byte("4444"))
		conn.Write([]byte("55555"))
		conn.Write([]byte("666666"))
	}
	time.Sleep(time.Second)
}

先运行服务端,然后再运行客户端,可以看到服务端的日志如下:

received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666

从我们这个日志,我们看到收到的消息是串了的,那么为什么会出现此问题呢?

这是因为 TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息。

粘包的主要原因是

  1. 发送方每次写入数据 < 套接字(Socket)缓冲区大小
  2. 接收方读取套接字(Socket)缓冲区数据不够及时,造成积压

拆包的主要原因:

  1. 发送方每次写入数据 > 套接字(Socket)缓冲区大小
  2. 发送的数据大于协议的MTU(Maximum Transmission Unit,最大传输单元),因此必须拆包

对于粘包和拆包的有如下解决方案

  1. 使用特定字符来分割数据包,但是若数据中含有分割字符则会出现Bug
  2. 定长分隔(每个数据包最大为该长度,不足时使用特殊字符填充) ,但是数据不足时会浪费传输资源
  3. 在数据包中添加长度字段,自定义协议实现,golang可以使用bufio.Scanner包来实现自定义协议

只有在直接使用 TCP 协议才存在 “粘包/拆包” 问题,其上层应用层协议比如 HTTP ,已经帮我们处理好了,无需关注这些底层,但是我们自己实现一个自定义协议,就必须考虑这些细节了

如果是UDP的通信呢,需要不需要考虑 ”粘包/拆包” 问题呢??

  1. TCP,Transmission Control Protocol,传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议
  2. UDP,User Datagram Protocol,用户数据包协议,是面向无连接,不可靠的,基于数据报的传输层通信协议

从上面的定义来可以猜一猜.

发表在 好文推荐 | 标签为 | 4 条评论