引言
上一篇文章『 要疯了,到底什么是网络编程?』,我们用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 {
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
我们首先通过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》
- 《计算机网络》
希望大家喜欢,原创文章不易,麻烦大家关注
,在看
,转发
一键三连,谢谢大家。希望通过代码+图片的方式,教大家学看得见的网络编程。做不了火影主角,做个掌握核心科技的“蛇叔”也不错哈。