丢掉 nc,自己实现 echo 客户端
引言
上一篇文章『 要疯了,到底什么是网络编程? 』,我们用 Go
实现了自己的 echo服务器
,并且使用 nc
伪装 echo客户端
和我们自己写的 echo服务器
进行了收发数据交互,并对这一过程进行了详细的讲解。这一节我们将用 Go
实现自己的 echo客户端
, Let's go
。
目录
设计思路
-
使用 Go语言 开发我们的
echo客户端
,最小使用 Go语言 的原生net
网络库,从而直击网络编程
的本质。 -
从 标准输入 读取数据,发往 服务器 ,读取 服务器 返回的数据,打印到 标准输出 。
-
注意 读写数据 细节问题。
echo客户端代码
/** * File: echoClient.go * Author: 蛇叔 * 公众号: 蛇叔编程心法 */ package main import ( "bufio" "fmt" "net" "os" "syscall" ) const ( PORT = 8888 ADDR = "127.0.0.1" SIZE = 100 ) func main() { // 1. 建立socket socketFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil || socketFd 0 { readn, err2 =syscall.Read(socketFd, buf) if readn > 0 { fmt.Println("read from socket: ") fmt.Println(string(buf[:readn])) } else { break } } else if writen <= 0 && err2 != nil { fmt.Printf("socket write; writen:%d, err: %s\n", writen, err2) break } } // 5. close socketFd _ = syscall.Close(socketFd) }
# 编译 go build -o echoClient echoCLient.go # 启动上一节的`echoServer` ./echoServer # 运行echoClient ./echoClient # 发送字符串,打印返回 hello-echo read from socket: hello-echo
交互详解
上一篇文章,我们画了 echo服务器
和 echo客户端
详细的交互过程。
echo客户端
并不需要 bind() , listen() ,想一想这是为什么呢?
其实 bind 会将当前 socket
和一个端口相绑定,这样就限制了客户端的自由性。假如你要在一台机器启动多个 echoClient
客户端,如果 bind
了一个端口,那么第二个 echoClient
就启动不了了。至于 listen 只有在被动连接的时候才需要监听套接字, echoClient
客户端无疑是需要主动发起连接的。在 C/S
架构中,也一定是客户端主动发起连接。
建立 socket
内核数据结构
// 1. 建立socket socketFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil || socketFd < 0 { fmt.Println("socket create err: ", err) os.Exit(-1) }
echoClient
第一步就是建立 socket
内核数据结构,并绑定一个 file
。都说 Linux一切皆文件
。那如何才能观察到这个文件呢?
首先我们把之后的代码都忽略掉。当新建了一个 socket
内核数据结构后,给我们返回一个 socketFd
。我们在后边加一行 for {}
。如下:
// 1. 建立socket socketFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil || socketFd < 0 { fmt.Println("socket create err: ", err) os.Exit(-1) } for { }
这时候,我们编译 go build -o echoClient echoClient.go
, 并执行 ./echoClient
。
[root@VM-16-9-centos ~]# ps -ef |head -1 ; ps -ef|grep echo |grep -v grep UID PID PPID C STIME TTY TIME CMD root 5662 3085 0 22:06 pts/0 00:00:00 ./echoServer root 5755 5689 0 22:06 pts/1 00:00:00 ./echoClient
我们首先通过 ps
和 grep
命令,找到了 echo客户端
的进程号为 5755 。
[root@VM-16-9-centos v1]# lsof -nP -p 5755 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME echoClien 5755 root cwd DIR 253,1 4096 1180315 /root/wx/v1 echoClien 5755 root rtd DIR 253,1 4096 2 / echoClien 5755 root txt REG 253,1 2035084 1051843 /root/wx/v1/echoClient echoClien 5755 root 0u CHR 136,3 0t0 6 /dev/pts/3 echoClien 5755 root 1u CHR 136,3 0t0 6 /dev/pts/3 echoClien 5755 root 2u CHR 136,3 0t0 6 /dev/pts/3 echoClien 5755 root 3u sock 0,7 0t0 24732864 protocol: TCP
之后,我们通过lsof命令查看 echoClient
进程打开的文件,这里我们着重关注最后一列
Linux中一切皆文件。 socket
套接字也不例外。 3u
中 3
表示的是这个 socket
文件描述符, u
表示这是一个读写方式打开的文件。 TYPE 列中的 sock
表示这是一个 socket
文件类型。最后的 TCP
说明该 sock
是基于 Tcp协议
的。
发起主动握手
去掉之前的 for{}
代码,我们正常编译执行一下。echoClient调用 Connect 发起3次握手的主动连接,也就是会给对端发送一个 SYN
同步原语,等待 echoServer
收到后,发来 ACK,SYN
,之后 echoClient
发送 ACK
给 echoServer
。此时 Connect 返回, Connect
认为三次握手已经完成, echoClient
端的 TCP
状态变为 ESTABLISHED
。我们通过命令行工具lsof再查看一下。
[root@VM-16-9-centos ~]# lsof -nP -p 5755 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME echoClien 5755 root cwd DIR 253,1 4096 1180315 /root/wx/v1 echoClien 5755 root rtd DIR 253,1 4096 2 / echoClien 5755 root txt REG 253,1 2197037 1051837 /root/wx/v1/echoClient echoClien 5755 root mem REG 253,1 2156240 265623 /usr/lib64/libc-2.17.so echoClien 5755 root mem REG 253,1 142144 265649 /usr/lib64/libpthread-2.17.so echoClien 5755 root mem REG 253,1 163312 265614 /usr/lib64/ld-2.17.so echoClien 5755 root 0u CHR 136,1 0t0 4 /dev/pts/1 echoClien 5755 root 1u CHR 136,1 0t0 4 /dev/pts/1 echoClien 5755 root 2u CHR 136,1 0t0 4 /dev/pts/1 echoClien 5755 root 3u IPv4 24694342 0t0 TCP 127.0.0.1:52230->127.0.0.1:8888 (ESTABLISHED)
这里我们仍然着重关注最后一列:
3u
中 3
表示的是这个 socket
文件描述符, u
表示这是一个读写方式打开的文件。 TYPE 列中的 IPv4
和 NODE 的 TCP
表示这是一个基于 ipv4
的 tcp
类型的 socket
。最后的 Name 也唯一确定了一个 socket
的文件名。 52230
表示是 echoClient
客户端随机选择的一个端口号,目的地址是 127.0.0.1:8888
也正是我们的 echoServer
服务器地址。最后 ESTABLISHED
表示这个TCP连接是一个 ESTABLISHED
状态的连接,和我们预期的一样。在这里,我们通过 lsof
真正做到了看得见的 TCP
。平时说的 Linux一切皆文件
思想,也得到了真实的印证。
收发数据
writen, err2 = syscall.Write(socketFd, buf)
当3次握手成功后, echoClient
向 echoServer
写入数据,这里如果写入成功, writen
一定是 大于0 ,并且等于 len(buf)
的。因为这里用的是阻塞模式(非阻塞模式,以后的文章会讲,所以记得 关注 哦~~),如果 socket
发送缓冲区空闲空间不够,则 syscall.Write
会一只阻塞,直到 发送缓冲区 可以完全写入数据。如果 writen
返回 小于等于0 则发生了错误。需要关闭连接。值得一提的是,如果对端 echoServer
程序奔溃, echoServer
端内核协议栈会往客户端发送 FIN
,这时候 echoClient
read 的时候,会返回 0
,也就是 EOF
。这种情况,通常需要客户端关闭连接。
关闭 socket
最后调用 Close()
,关闭连接,这里 echoClient
发起主动关闭。也就是会给 echoServer
发送 FIN
。等待对端确认后,并发送过来 FIN
,我们回复一个 ACK
, 客户端会进入 TIME_WAIT
状态。
那么在 close()
之后,我们的 echoClient
客户端进程内的套机字是啥样的呢?我们在程序末尾加上 for{}
,像之前一样,编译执行,等待一会,我们再通过 lsof
观察一下 echoClent
客户端。
[root@VM-16-9-centos ~]# lsof -nP -p 5755 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME echoClien 5755 root cwd DIR 253,1 4096 1180315 /root/wx/v1 echoClien 5755 root rtd DIR 253,1 4096 2 / echoClien 5755 root txt REG 253,1 2197037 1051837 /root/wx/v1/echoClient echoClien 5755 root mem REG 253,1 2156240 265623 /usr/lib64/libc-2.17.so echoClien 5755 root mem REG 253,1 142144 265649 /usr/lib64/libpthread-2.17.so echoClien 5755 root mem REG 253,1 163312 265614 /usr/lib64/ld-2.17.so echoClien 5755 root 0u CHR 136,1 0t0 4 /dev/pts/1 echoClien 5755 root 1u CHR 136,1 0t0 4 /dev/pts/1 echoClien 5755 root 2u CHR 136,1 0t0 4 /dev/pts/1
这时候,我们会看到,之前的 3u
已经不存在了,标明这个 socket
文件已经关闭了。
至此,我们的 echoClient
也分析完了。在 CS架构
中,客户端和服务器都是必不可少。比如我们的 安卓
或者 IOS
应用就是客户端,每时每刻都在和我们的服务端在做网络交互。可以说 网络编程
是我们互联网的基石。
上一篇和这一篇文章我们讲解了正常的网络交互程序,下一篇我们将通过 Wireshark 和 tcpdump 一步步来分析下我们的 echo客户端/服务器程序
,并对一些可能的异常情况进行分析,希望大家 多多关注 ,我们下期再见。
参考文献
-
《TCP/IP详解 卷1》
-
《Unix网络编程 卷1》
-
《计算机网络》
希望大家喜欢,原创文章不易,麻烦大家 关注
, 在看
, 转发
一键三连,谢谢大家。希望通过 代码+图片 的方式,教大家学 看得见的网络编程 。做不了火影主角,做个掌握核心科技的“蛇叔”也不错哈 。