Go语言文件操作

Python014

Go语言文件操作,第1张

本文主要介绍了Go语言中文件读写的相关操作。

文件是什么?

计算机中的文件是存储在外部介质(通常是磁盘)上的数据集合,文件分为文本文件和二进制文件。

os.Open() 函数能够打开一个文件,返回一个 *File 和一个 err 。对得到的文件实例调用 close() 方法能够关闭文件。

为了防止文件忘记关闭,我们通常使用defer注册文件关闭语句。

Read方法定义如下:

它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回 0 和 io.EOF 。 举个例子:

使用for循环读取文件中的所有数据。

bufio是在file的基础上封装了一层API,支持更多的功能。

io/ioutil 包的 ReadFile 方法能够读取完整的文件,只需要将文件名作为参数传入。

os.OpenFile() 函数能够以指定模式打开文件,从而实现文件写入相关功能。

其中:

name :要打开的文件名 flag :打开文件的模式。 模式有以下几种:

perm :文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。

三次握手:

            1. 主动发起连接请求端(客户端),发送 SYN 标志位,携带数据包、包号

            2. 被动接收连接请求端(服务器),接收 SYN,回复 ACK,携带应答序列号。同时,发送SYN标志位,携带数据包、包号

            3. 主动发起连接请求端(客户端),接收SYN 标志位,回复 ACK。

                        被动端(服务器)接收 ACK —— 标志着 三次握手建立完成( Accept()/Dial() 返回 )

四次挥手:

            1. 主动请求断开连接端(客户端), 发送 FIN标志,携带数据包

            2. 被动接受断开连接端(服务器), 发送 ACK标志,携带应答序列号。 —— 半关闭完成。

            3. 被动接受断开连接端(服务器), 发送 FIN标志,携带数据包

            4. 主动请求断开连接端(客户端), 发送 最后一个 ACK标志,携带应答序列号。—— 发送完成,客户端不会直接退出,等 2MSL时长。

                        等 2MSL待目的:确保服务器 收到最后一个ACK

滑动窗口:

            通知对端本地存储数据的 缓冲区容量。—— write 函数在对端 缓冲区满时,有可能阻塞。

TCP状态转换:

            1. 主动发起连接请求端:

                        CLOSED ——>发送SYN ——>SYN_SENT(了解) ——>接收ACK、SYN,回发 ACK ——>ESTABLISHED (数据通信)

            2. 主动关闭连接请求端:

                        ESTABLISHED ——>发送FIN ——>FIN_WAIT_1 ——>接收ACK ——>FIN_WAIT_2 (半关闭、主动端)

                        ——>接收FIN、回复ACK ——>TIME_WAIT (主动端) ——>等 2MSL 时长 ——>CLOSED

            3. 被动建立连接请求端:

                        CLOSED ——>LISTEN ——>接收SYN、发送ACK、SYN ——>SYN_RCVD ——>接收 ACK ——>ESTABLISHED (数据通信)

            4. 被动断开连接请求端:

                        ESTABLISHED ——>接收 FIN、发送 ACK ——>CLOSE_WAIT ——>发送 FIN ——>LAST_ACK ——>接收ACK ——>CLOSED

windows下查看TCP状态转换:

            netstat -an | findstr  端口号

Linux下查看TCP状态转换:

            netstat -an | grep  端口号

TCP和UDP对比: 

            TCP: 面向连接的可靠的数据包传递。 针对不稳定的 网络层,完全弥补。ACK

            UDP:无连接不可靠的报文传输。 针对不稳定的 网络层,完全不弥补。还原网络真实状态。

                                    优点                                                            缺点

            TCP: 可靠、顺序、稳定                                      系统资源消耗大,程序实现繁复、速度慢

            UDP:系统资源消耗小,程序实现简单、速度快                          不可靠、无序、不稳定

使用场景:

            TCP:大文件、可靠数据传输。 对数据的 稳定性、准确性、一致性要求较高的场合。

            UDP:应用于对数据时效性要求较高的场合。 网络直播、电话会议、视频直播、网络游戏。

UDP-CS-Server实现流程:

            1.  创建 udp地址结构 ResolveUDPAddr(“协议”, “IP:port”) ——>udpAddr 本质 struct{IP、port}

            2.  创建用于 数据通信的 socket ListenUDP(“协议”, udpAddr ) ——>udpConn (socket)

            3.  从客户端读取数据,获取对端的地址 udpConn.ReadFromUDP() ——>返回:n,clientAddr, err

            4.  发送数据包给 客户端 udpConn.WriteToUDP("数据", clientAddr)

UDP-CS-Client实现流程:

            1.  创建用于通信的 socket。 net.Dial("udp", "服务器IP:port") ——>udpConn (socket)

            2.  以后流程参见 TCP客户端实现源码。

UDPserver默认就支持并发!

------------------------------------

命令行参数: 在main函数启动时,向整个程序传参。 【重点】

            语法: go run xxx.go   argv1 argv2  argv3  argv4 。。。

                        xxx.exe:  第 0 个参数。

                        argv1 :第 1 个参数。

                        argv2 :第 2 个参数。

                        argv3 :第 3 个参数。

                        argv4 :第 4 个参数。

            使用: list := os.Args  提取所有命令行参数。

获取文件属性函数:

            os.stat(文件访问绝对路径) ——>fileInfo 接口

            fileInfo 包含 两个接口。

                        Name() 获取文件名。 不带访问路径

                        Size() 获取文件大小。

网络文件传输 —— 发送端(客户端)

            1.  获取命令行参数,得到文件名(带路径)filePath list := os.Args

            2.  使用 os.stat() 获取 文件名(不带路径)fileName

            3.  创建 用于数据传输的 socket  net.Dial("tcp", “服务器IP+port”) —— conn

            4.  发送文件名(不带路径)  给接收端, conn.write()

            5.  读取 接收端回发“ok”,判断无误。封装函数 sendFile(filePath, conn) 发送文件内容

            6.  实现 sendFile(filePath,  conn)

                        1) 只读打开文件 os.Open(filePath)

                                    for {

                                    2) 从文件中读数据  f.Read(buf)

                                    3) 将读到的数据写到socket中  conn.write(buf[:n])

                                    4)判断读取文件的 结尾。 io.EOF. 跳出循环

                                    }

网络文件传输 —— 接收端(服务器)

            1. 创建用于监听的 socket net.Listen() —— listener

            2. 借助listener 创建用于 通信的 socket listener.Accpet()  —— conn

            3. 读取 conn.read() 发送端的 文件名, 保存至本地。

            4. 回发 “ok”应答 发送端。

            5. 封装函数,接收文件内容 recvFile(文件路径)

                        1) f = os.Create(带有路径的文件名)

                        for {

                        2)从 socket中读取发送端发送的 文件内容 。 conn.read(buf)

                        3)  将读到的数据 保存至本地文件 f.Write(buf[:n])

                        4)  判断 读取conn 结束, 代表文件传输完成。 n == 0  break

                        }

            

一、关于连接池

一个数据库服务器只拥有有限的资源,并且如果你没有充分使用这些资源,你可以通过使用更多的连接来提高吞吐量。一旦所有的资源都在使用,那么你就不 能通过增加更多的连接来提高吞吐量。事实上,吞吐量在连接负载较大时就开始下降了。通常可以通过限制与可用的资源相匹配的数据库连接的数量来提高延迟和吞 吐量。

如何在Go语言中使用Redis连接池

如果不使用连接池,那么,每次传输数据,我们都需要进行创建连接,收发数据,关闭连接。在并发量不高的场景,基本上不会有什么问题,一旦并发量上去了,那么,一般就会遇到下面几个常见问题:

性能普遍上不去

CPU 大量资源被系统消耗

网络一旦抖动,会有大量 TIME_WAIT 产生,不得不定期重启服务或定期重启机器

服务器工作不稳定,QPS 忽高忽低

要想解决这些问题,我们就要用到连接池了。连接池的思路很简单,在初始化时,创建一定数量的连接,先把所有长连接存起来,然后,谁需要使用,从这里取走,干完活立马放回来。 如果请求数超出连接池容量,那么就排队等待、退化成短连接或者直接丢弃掉。

二、使用连接池遇到的坑

最近在一个项目中,需要实现一个简单的 Web Server 提供 Redis 的 HTTP interface,提供 JSON 形式的返回结果。考虑用 Go 来实现。

首先,去看一下 Redis 官方推荐的 Go Redis driver。官方 Star 的项目有两个:Radix.v2 和 Redigo。经过简单的比较后,选择了更加轻量级和实现更加优雅的 Radix.v2。

Radix.v2 包是根据功能划分成一个个的 sub package,每一个 sub package 在一个独立的子目录中,结构非常清晰。我的项目中会用到的 sub package 有 redis 和 pool。

由于我想让这种被 fork 的进程最好简单点,做的事情单一一些,所以,在没有深入去看 Radix.v2 的 pool 的实现之前,我选择了自己实现一个 Redis pool。(这里,就不贴代码了。后来发现自己实现的 Redis pool 与 Radix.v2 实现的 Redis pool 的原理是一样的,都是基于 channel 实现的, 遇到的问题也是一样的。)

不过在测试过程中,发现了一个诡异的问题。在请求过程中经常会报 EOF 错误。而且是概率性出现,一会有问题,一会又好了。通过反复的测试,发现 bug 是有规律的,当程序空闲一会后,再进行连续请求,会发生3次失败,然后之后的请求都能成功,而我的连接池大小设置的是3。再进一步分析,程序空闲300秒 后,再请求就会失败,发现我的 Redis server 配置了 timeout 300,至此,问题就清楚了。是连接超时 Redis server 主动断开了连接。客户端这边从一个超时的连接请求就会得到 EOF 错误。

然后我看了一下 Radix.v2 的 pool 包的源码,发现这个库本身并没有检测坏的连接,并替换为新server{location/pool{content_by_lua_block{localredis=require"resty.redis"localred=redis:new()localok,err=red:connect("127.0.0.1",6379)ifnotokthenngx.say("failedtoconnect:",err)returnendok,err=red:set("hello","world")ifnotokthenreturnendred:set_keepalive(10000,100)}}}

发现有个 set_keepalive 的方法,查了一下官方文档,方法的原型是 syntax: ok, err = red:set_keepalive(max_idle_timeout, pool_size) 貌似 max_idle_timeout 这个参数,就是我们所缺少的东西,然后进一步跟踪源码,看看里面是怎么保证连接有效的。

function_M.set_keepalive(self,...)localsock=self.sockifnotsockthenreturnnil,"notinitialized"endifself.subscribedthenreturnnil,"subscribedstate"endreturnsock:setkeepalive(...)end

至此,已经清楚了,使用了 tcp 的 keepalive 心跳机制。

于是,通过与 Radix.v2 的作者一些讨论,选择自己在 redis 这层使用心跳机制,来解决这个问题。

四、最后的解决方案

在创建连接池之后,起一个 goroutine,每隔一段 idleTime 发送一个 PING 到 Redis server。其中,idleTime 略小于 Redis server 的 timeout 配置。连接池初始化部分代码如下:

p,err:=pool.New("tcp",u.Host,concurrency)errHndlr(err)gofunc(){for{p.Cmd("PING")time.Sleep(idelTime*time.Second)}}()

使用 redis 传输数据部分代码如下:

funcredisDo(p*pool.Pool,cmdstring,args...interface{})(reply*redis.Resp,errerror){reply=p.Cmd(cmd,args...)iferr=reply.Errerr!=nil{iferr!=io.EOF{Fatal.Println("redis",cmd,args,"erris",err)}}return}

其中,Radix.v2 连接池内部进行了连接池内连接的获取和放回,代码如下:

//Cmdautomaticallygetsoneclientfromthepool,executesthegivencommand//(returningitsresult),andputstheclientbackinthepoolfunc(p*Pool)Cmd(cmdstring,args...interface{})*redis.Resp{c,err:=p.Get()iferr!=nil{returnredis.NewResp(err)}deferp.Put(c)returnc.Cmd(cmd,args...)}

这样,我们就有了 keepalive 的机制,不会出现 timeout 的连接了,从 redis 连接池里面取出的连接都是可用的连接了。看似简单的代码,却完美的解决了连接池里面超时连接的问题。同时,就算 Redis server 重启等情况,也能保证连接自动重连。