基于TCP实现简单的聊天室
原文出处:《Go 语言编程之旅》第四章4.1节
基于TCP的聊天室
1、服务端
- 新用户到来,生成一个User的实例,代表该用户。
type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
}
- 新开一个goroutine用于给用户发送消息
func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
}
结合User结构体的MessageChannel,很容易知道,需要给某个用户发送消息,只需要往该用户的MessageChannel中写入消息即可。这里需要特别提醒下,因为sendMessage在一个新的goroutine中,如果函数的ch不关闭,该goroutine是不会退出的,因此需要注意不关闭ch导致goroutine泄露问题。
- 给当前用户发送欢迎信息,同时给聊天室所有的用户发送有新用户到来的提醒
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg
- 将该新用户写入全局用户列表,也就是聊天室用户列表。同时控制用户超时退出,超过5分钟没有任何响应,则提出
enteringChannel <- user
// 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}()
读取用户的输入,并将用户信息发送给其他用户。
在bufio包中有多重方式获取文本输入,ReadBytes、ReadString和独特的ReadLine,对于简单的目的这些都有些复杂。在Go1,1中添加了一个新类型,Scabber,以便更容易的处理如按行读取输入序列或空格分隔单词等这类简单任务。它终结了如输入一个很长的有问题的行这样的输入错误,并且提供了简单的默认行为:基于行的输入,每行都提出了分隔标识。
// 循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg
// 用户活跃
userActive <- struct{}{}
}
if err := input.Err();err != nil {
log.Println("读取错误:", err)
}
- 用户离开,需要做登记,并给连天使其他用户发通知
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
完整代码
package main
import (
"bufio"
"fmt"
"log"
"net"
"strconv"
"sync"
"time"
)
type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
}
// 给用户发送信息
type Message struct{
OwnerID int
Content string
}
var (
// 新用户到来,通过该channel进行登记
enteringChannel = make(chan *User)
// 用户离开,通过该channel进行登记
leavingChannel = make(chan *User)
// 广播专用的用户普通消息channel, 缓冲是尽可能避免出现异常情况阻塞
messageChannel = make(chan Message, 9)
)
func (u *User) String() string{
return u.Addr + ",UID:" + strconv.Itoa(u.ID) + ", Enter At:" + u.EnterAt.Format("2006-01-02 15:04:05+8000")
}
func main() {
listener, err := net.Listen("tcp",":2020")
if err != nil {
panic(err)
}
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConn(conn)
}
}
// broadcaster 用于记录聊天室用户,并进行消息广播
// 1. 新用户进来; 2.用户普通消息; 3.用户离开
func broadcaster(){
users := make(map[*User]struct{})
for {
select{
case user := <- enteringChannel:
// 新用户进入
users[user] = struct{}{}
case user := <- leavingChannel:
// 用户离开
delete(users, user)
// 避免goroutine泄露
close(user.MessageChannel)
case msg := <-messageChannel:
// 给所有在线用户发送消息
for user := range users {
if user.ID == msg.OwnerID{
continue
}
user.MessageChannel <- msg.Content
}
}
}
}
func handleConn(conn net.Conn){
defer conn.Close()
// 1. 新用户进来,构建该用户实例
user := &User{
ID: GenUserID(),
Addr: conn.RemoteAddr().String(),
EnterAt: time.Now(),
MessageChannel: make(chan string,8),
}
// 2. 当前在一个新的goroutine 中,用来进行读写操作,因此需要开一个goroutine用于读写操作
// 读写goroutine 之间通过channel 进行通信
go sendMessage(conn, user.MessageChannel)
// 3. 给当前用户发送欢迎信息;给所有用户告知新用户列表
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg
// 4. 将该记录到全局的用户列表中,避免用锁
enteringChannel <- user
// 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}()
// 5. 循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg
// 用户活跃
userActive <- struct{}{}
}
if err := input.Err();err != nil {
log.Println("读取错误:", err)
}
// 6. 用户离开
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
}
func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
}
// 生成用户id
var (
globalID int
idocker sync.Mutex
)
func GenUserID() int {
idocker.Lock()
defer idocker.Unlock()
globalID ++
return globalID
}
2、客户端
客户端的实现直接采用 《The Go Programming Language》一书对应的示例源码:ch8/netcat3/netcat.go 。
func main() {
conn, err := net.Dial("tcp", ":2020")
if err != nil {
panic(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // NOTE: ignoring errors
log.Println("done")
done <- struct{}{} // signal the main goroutine
}()
mustCopy(conn, os.Stdin)
conn.Close()
<-done
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
- 新开了一个 goroutine 用于接收消息;
- 通过 io.Copy 来操作 IO,包括从标准输入读取数据写入 TCP 连接中,以及从 TCP 连接中读取数据写入标准输出;
- 新开的 goroutine 通过一个 channel 来和 main goroutine 通讯;
基于TCP实现简单的聊天室的更多相关文章
- 基于TCP/IP的局域网聊天室---C语言
具备注册账号,群聊,查看在线人员信息,私发文件和接收文件功能,因为每个客户端只有一个属于自己的socket,所以无论客户端是发聊天消息还是文件都是通过这一个socket发送, 这也意味着服务器收发任何 ...
- 基于websocket实现的一个简单的聊天室
本文是基于websocket写的一个简单的聊天室的例子,可以实现简单的群聊和私聊.是基于websocket的注解方式编写的.(有一个小的缺陷,如果用户名是中文,会乱码,不知如何处理,如有人知道,请告知 ...
- 基于LINUX的多功能聊天室
原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来 ...
- Android简单的聊天室开发(client与server沟通)
请尊重他人的劳动成果.转载请注明出处:Android开发之简单的聊天室(client与server进行通信) 1. 预备知识:Tcp/IP协议与Socket TCP/IP 是Transmission ...
- Netty学习笔记(四) 简单的聊天室功能之服务端开发
前面三个章节,我们使用了Netty实现了DISCARD丢弃服务和回复以及自定义编码解码,这篇博客,我们要用Netty实现简单的聊天室功能. Ps: 突然想起来大学里面有个课程实训,给予UDP还是TCP ...
- 基于EPOLL模型的局域网聊天室和Echo服务器
一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说sel ...
- 基于 OpenResty 实现一个 WS 聊天室
基于 OpenResty 实现一个 WS 聊天室 WebSocket WebSocket 协议分析 WebSocket 协议解决了浏览器和服务器之间的全双工通信问题.在WebSocket出现之前,浏览 ...
- 玩转Node.js(四)-搭建简单的聊天室
玩转Node.js(四)-搭建简单的聊天室 Nodejs好久没有跟进了,最近想用它搞一个聊天室,然后便偶遇了socket.io这个东东,说是可以用它来简单的实现实时双向的基于事件的通讯机制.我便看了一 ...
- TCP/IP以及Socket聊天室带类库源码分享
TCP/IP以及Socket聊天室带类库源码分享 最近遇到个设备,需要去和客户的软件做一个网络通信交互,一般的我们的上位机都是作为客户端来和设备通信的,这次要作为服务端来监听客户端,在这个背景下,我查 ...
- 简单的聊天室代码php+swoole
php swoole+websocket 客户端代码 <!DOCTYPE html> <html> <head> <title></title&g ...
随机推荐
- HTTP 尝试获取 Client IP
HTTP 中获取 Client IP 相关策略需求, 在当下网络环境中多数只能提供建议作用. 更多的是 通过其它唯一标识来挖掘更多潜在价值. 本文主要就一个内容, 如何最大可能尝试在 HTTP 请求中 ...
- grpc unable to determine Go import path for
前言 在 proto 文件夹下执行如下命令: $ protoc --go_out=plugins=grpc:. *.proto 报错:无法确定Go导入路径 protoc-gen-go: unable ...
- php使用redis锁
redis加锁分类 redis能用的的加锁命令分别是INCR.SETNX.SET 利用predis操作redis方法大全 第一种锁命令INCR 这种加锁的思路是, 当 key 不存在,那么 key 的 ...
- etcd 快速入门
一.认识etcd 1.1 etcd 概念 从哪里说起呢?官网第一个页面,有那么一句话: "A distributed, reliable key-value store for the mo ...
- 哈希表(实现 Python 中的集合 set)
博客地址:https://www.cnblogs.com/zylyehuo/ # -*- coding: utf-8 -*- class LinkList: class Node: def __ini ...
- 深度剖析 StarRocks 读取 ORC 加密文件背后的技术
作者:vivo 互联网大数据团队 - Zheng Xiaofeng 本文介绍了StarRocks数据库如何读取ORC加密文件,包括基础概念以及具体实现方案.深入探讨了利用ORC文件的四层结构和三层索引 ...
- 思绪碎片:一个INFP的自我对话
## 关于存在与意义 > "我写一些东西,不是为了让别人看见,而是为了未来的我." -- 阮一峰 - 未知带来的心慌持续蔓延 - **根本症结**:自身的弱小,无法坦然面对生 ...
- 从零开始:在Qt中使用OpenGL绘制指南
本文只介绍基本的 QOpenGLWidget 和 QOpenGLFunctions 的使用,想要学习 OpenGL 的朋友,建议访问经典 OpenGL 学习网站:LearnOpenGL CN 本篇文章 ...
- CH39x产品介绍
CH39x产品介绍 芯片 CH392 CH395 CH390 接口 异步串口/SPI 异步串口/SPI/8位并口(仅CH395L支持) SPI/16位或8位并口(仅CH390L支持) 网口速率 10M ...
- MYSQL数据库 MariaDB断电恢复总结
背景:本次是机房异常断电,导致数据库文件损坏.在数据库自启动之后频繁宕机,在多次尝试以后,总结了一下几种方法,及供参考. 1.mariadb服务器断电重启之后Missing MLOG_CHECKPOI ...