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

.container .card .information strong · · 664 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

图片

图片

引言

上一篇文章『要疯了,到底什么是网络编程?』,我们用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 {
        fmt.Println("socket create err: ", err)
        os.Exit(-1)
    }
    ip4 := net.ParseIP(ADDR).To4()
    if ip4 == nil {
        fmt.Println("net.ParseIP err")
        os.Exit(-1)
    }
    sa := &syscall.SockaddrInet4{Port:PORT}
    copy(sa.Addr[:], ip4)
    // 2. 发起主动连接
    err = syscall.Connect(socketFd, sa)
    if err != nil {
        fmt.Println("socket connect err: ", err)
        os.Exit(-1)
    }
    var (
        bufReader = bufio.NewReader(os.Stdin)
        buf = make([]byte, SIZE)
        writen int
        readn int
        err2 error
    )
    for {
        // 3. 从标准输入读取数据
        line, _, err := bufReader.ReadLine()
        if err != nil {
            fmt.Println("bufReader.ReadLine err: ", err)
            break
        }
        buf = line[:]
        // 4. 向socket对端写入数据
        writen, err2 = syscall.Write(socketFd, buf)
        if writen > 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: %sn", 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,这时候echoClientread的时候,会返回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. 《计算机网络》

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


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:.container .card .information strong

查看原文:02.丢掉nc,自己实现echo客户端

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

664 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传