丢掉 nc,自己实现 echo 客户端

引言

上一篇文章『 要疯了,到底什么是网络编程? 』,我们用 Go 实现了自己的 echo服务器 ,并且使用 nc 伪装 echo客户端 和我们自己写的 echo服务器 进行了收发数据交互,并对这一过程进行了详细的讲解。这一节我们将用 Go 实现自己的 echo客户端Let's go

目录

设计思路

  1. 使用 Go语言 开发我们的 echo客户端 ,最小使用 Go语言 的原生 net 网络库,从而直击 网络编程 的本质。

  2. 标准输入 读取数据,发往 服务器 ,读取 服务器 返回的数据,打印到 标准输出

  3. 注意 读写数据 细节问题。

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

我们首先通过 psgrep 命令,找到了 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 套接字也不例外。 3u3 表示的是这个 socket 文件描述符, u 表示这是一个读写方式打开的文件。 TYPE 列中的 sock 表示这是一个 socket 文件类型。最后的 TCP 说明该 sock 是基于 Tcp协议 的。

发起主动握手

去掉之前的 for{} 代码,我们正常编译执行一下。echoClient调用 Connect 发起3次握手的主动连接,也就是会给对端发送一个 SYN 同步原语,等待 echoServer 收到后,发来 ACK,SYN ,之后 echoClient 发送 ACKechoServer 。此时 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)

这里我们仍然着重关注最后一列:

3u3 表示的是这个 socket 文件描述符, u 表示这是一个读写方式打开的文件。 TYPE 列中的 IPv4NODETCP 表示这是一个基于 ipv4tcp 类型的 socket 。最后的 Name 也唯一确定了一个 socket 的文件名。 52230 表示是 echoClient 客户端随机选择的一个端口号,目的地址是 127.0.0.1:8888 也正是我们的 echoServer 服务器地址。最后 ESTABLISHED 表示这个TCP连接是一个 ESTABLISHED 状态的连接,和我们预期的一样。在这里,我们通过 lsof 真正做到了看得见的 TCP 。平时说的 Linux一切皆文件 思想,也得到了真实的印证。

收发数据

writen, err2 = syscall.Write(socketFd, buf)

当3次握手成功后, echoClientechoServer 写入数据,这里如果写入成功, 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 应用就是客户端,每时每刻都在和我们的服务端在做网络交互。可以说 网络编程 是我们互联网的基石。

上一篇和这一篇文章我们讲解了正常的网络交互程序,下一篇我们将通过 Wiresharktcpdump 一步步来分析下我们的 echo客户端/服务器程序 ,并对一些可能的异常情况进行分析,希望大家 多多关注 ,我们下期再见。

参考文献

  1. 《TCP/IP详解 卷1》

  2. 《Unix网络编程 卷1》

  3. 《计算机网络》

希望大家喜欢,原创文章不易,麻烦大家 关注在看转发 一键三连,谢谢大家。希望通过 代码+图片 的方式,教大家学 看得见的网络编程 。做不了火影主角,做个掌握核心科技的“蛇叔”也不错哈 。