1. 程式人生 > 實用技巧 >02.丟掉nc,自己實現echo客戶端

02.丟掉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 {
		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: %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,這時候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. 《計算機網路》

希望大家喜歡,原創文章不易,麻煩大家關注在看轉發一鍵三連,謝謝大家。希望通過程式碼+圖片的方式,教大家學看得見的網路程式設計。做不了火影主角,做個掌握核心科技的“蛇叔”也不錯哈