8채널, 리플렉션, 네트워크 프로그래밍 [Go Language Tutorial]
1 채널
errChan := make(chan err) 파이프 길이를 지정하지 않으면 버퍼링되지 않은 채널이 생성됩니다.
- 버퍼링되지 않은 채널의 경우 전송 및 수신 작업이 동기식입니다. 보내기 작업은 수신자가 값을 받을 준비가 될 때까지 대기하고 수신 작업은 보낸 사람이 값을 보낼 준비가 될 때까지 기다립니다. 따라서 교착 상태를 방지하려면 버퍼링되지 않은 채널에서 전송 및 수신 작업을 별도의 코루틴에서 수행해야 합니다.
Go에서 채널은 고정 길이이며 일단 생성되면 변경할 수 없습니다. make 함수를 사용하여 채널을 만들 때 선택적으로 채널의 용량(길이)을 지정할 수 있습니다.
채널의 용량을 지정하지 않으면 기본적으로 버퍼링되지 않은 채널, 즉 길이가 0인 채널이 됩니다. 버퍼링되지 않은 채널은 코루틴이 값을 받을 준비가 될 때까지 전송 작업을 차단하고 코루틴이 값을 보낼 준비가 될 때까지 수신 작업을 차단합니다.
버퍼링되지 않은 채널의 경우 전송 및 수신 작업이 동기식입니다. 보내기 작업은 수신자가 값을 받을 준비가 될 때까지 대기하고 수신 작업은 보낸 사람이 값을 보낼 준비가 될 때까지 기다립니다. 따라서 교착 상태를 방지하려면 버퍼링되지 않은 채널에서 전송 및 수신 작업을 별도의 코루틴에서 수행해야 합니다.
버퍼링된 채널의 경우 채널을 만들 때 채널의 용량을 지정할 수 있습니다. 버퍼링된 채널을 사용하면 즉시 차단하지 않고 작업을 보낼 수 있으며 채널의 버퍼가 가득 찬 경우에만 차단됩니다. 마찬가지로 수신 작업에서는 채널의 버퍼가 비어 있는 경우에만 차단됩니다.
채널의 용량은 채널의 버퍼 크기에만 영향을 미치며 채널이 보유할 수 있는 값의 수에는 영향을 미치지 않습니다. 채널의 용량에 관계없이 수신자가 적시에 값을 수신하는 한 채널에 값을 얼마든지 보낼 수 있습니다.
요약하자면, 채널의 용량은 채널이 저장할 수 있는 값의 개수가 아니라 채널의 버퍼 크기를 의미합니다. 버퍼링되지 않은 채널은 용량이 0인 반면 버퍼링된 채널은 0보다 큰 용량을 지정할 수 있습니다.
1.1 개념 및 빠른 시작
channel:管道,主要用于不同goroutine之间的通讯
요구 사항: 이제 1-200에서 각 숫자의 계승을 계산하고 각 숫자의 계승을 맵에 넣습니다. 마지막으로 표시됩니다. 요청은 고루틴을 사용하여 수행됩니다.
아이디어 분석:
- goroutine을 사용하여 완료하는 것이 효율적이지만 동시성/병렬 보안 문제가 있습니다.
- 이것은 서로 다른 goroutine이 어떻게 통신하는지에 대한 질문을 제기합니다.
package main
import(
"fmt"
"time"
)
var (
myMap = make(map[int]int, 10)
)
//计算n!,将计算结果放入myMap
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res //concurrent map writes?
}
func main(){
//开启200个协程
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10s[防止主线程直接跑完,而协程中的任务未完成]
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
코드를 실행할 때 동시성 문제가 발생합니다.
서로 다른 고루틴 간에 통신하는 방법:
- 전역 변수에 대한 뮤텍스
- 파이프라인 채널을 사용하여 해결
1에서 200까지 각 숫자의 계승을 계산하고 각 숫자의 계승을 맵에 넣습니다. 마지막으로 표시됩니다. 다음과 같이 개선하십시오.
① 글로벌 변수를 이용한 뮤텍스 잠금[동시성, 병렬성 문제]
전역 변수 m이 추가되지 않으면 리소스 경합 문제가 발생하여 다음과 같은 메시지가 표시됩니다. 동시 맵 쓰기
뮤텍스에 가입
숫자의 계승이 매우 커서 결과가 범위를 벗어납니다. 계승을 sum += i로 변경할 수 있습니다.
package main
import(
"fmt"
"time"
"sync"
)
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
//sync 是包, synchronized 同步
//Mutex:是互斥
lock sync.Mutex
)
//计算n!,将计算结果放入myMap
func test(n int){
res := 1
for i := 1; i <= n; i++ {
res += int(i)
}
//加锁
lock.Lock()
myMap[n] = res //concurrent map writes?
//解锁
lock.Unlock()
}
func main(){
//开启200个协程
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10s[防止主线程直接跑完,而协程中的任务未完成]
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
② 채널 사용
1. 채널이 필요한 이유
- 왜 채널이 필요한가요?
- 기존에는 고루틴 통신을 해결하기 위해 전역 변수 잠금 동기화를 사용했지만 완벽하지는 않았습니다.
- 모든 고루틴이 완료될 때까지 메인 스레드가 대기하는 시간을 결정하기는 어려우므로 여기에서 설정한 10초는 추정치일 뿐입니다.
- 메인 스레드가 오랫동안 잠들면 대기 시간이 길어지고, 대기 시간이 짧으면 여전히 작업 상태에 있는 고루틴이 있을 수 있으며 메인 스레드가 종료될 때 소멸됩니다.
- 통신은 전역 변수를 잠그고 동기화하여 이루어지며 여러 코루틴을 사용하여 전역 변수를 읽고 쓰지 않습니다.
- 위의 모든 분석은 새로운 커뮤니케이션 메커니즘인 채널을 요구하고 있습니다.
2. 기본 소개
- channle의 본질은 데이터 구조 - 대기열 [개략도]
- 데이터는 선입 선출[FIFO: 선입 선출]입니다.
- 스레드 안전성, 여러 고루틴이 액세스할 때 잠글 필요가 없습니다. 즉, 채널 자체가 스레드로부터 안전합니다.
- 채널에는 유형이 있으며 문자열 채널은 문자열 유형 데이터만 저장할 수 있습니다.
3. 채널 선언 및 정의
- var 변수 이름 chan 데이터 유형
예:
var intChan chan int(intChan은 int 데이터를 저장하는 데 사용됨)
var mapChan chan map[int]string(mapChan은 map[int]문자열 유형을 저장하는 데 사용됨) var perChan chan Person var
perChan2 chan * 사람
…- 설명:
채널은 데이터를 쓰려면 채널을 초기화해야 하는 참조 유형입니다
. 즉, 채널은 make를 입력한 후에만 사용할 수 있으며 intChan은 정수 int만 쓸 수 있습니다.
4. 빠른 시작 및 주의사항
(1) 빠른 시작:
파이프라인 초기화, 파이프라인에 데이터 쓰기, 파이프라인에서 데이터 읽기 및 기본 주의사항
package main
import (
"fmt"
)
func main(){
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan的值=%v intChan本身地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan <- 10
num := 211
intChan <- num
intChan <- -50
//intChan <- 100 //注意:我们在给管道写入数据时,不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//5. 从管道中读取数据
var num2 int
num2 = <- intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//6. 在没有使用协程的情况下,如果管道中的数据已经全部取出,再取就会报告deadlock
num3 := <- intChan
num4 := <- intChan
fmt.Printf("num3=%v, num4=%v", num3, num4)
// num5 := <- intChan
// fmt.Println("num5=", num5) //fatal error: all goroutines are asleep - deadlock!
}
(2) 주의가 필요한 사항:
- 지정된 데이터 유형만 채널에 저장할 수 있습니다.
- 채널의 데이터가 가득 차면 더 이상 넣을 수 없습니다.
- 채널에서 데이터를 꺼내면 계속해서 넣을 수 있습니다.
- 코루틴을 사용하지 않는 경우 채널 데이터를 가져오고 다시 가져오면 데드락이 보고됨
5. 채널 사용
연습문제:
package main
import (
"fmt"
"math/rand"
"time"
"strconv"
)
type Person struct {
Name string
Age int
Address string
}
func main(){
var personChan chan Person
//make给chan开辟空间
personChan = make(chan Person, 10)
//取纳秒时间戳作为种子,保证每次的随机种子都不同
//给rand种种子
rand.Seed(time.Now().UnixNano())
for i := 1; i <= 10; i++ {
index := rand.Int()
fmt.Println("index===", index)
person := Person{
Name: "zhangsan" + strconv.Itoa(index),
Age: i,
Address: "beijing" + strconv.Itoa(index),
}
personChan <- person
}
len := len(personChan)
for i := 0; i < len; i++ {
p := <- personChan
fmt.Println(p)
}
}
6. 채널 폐쇄 및 순회
(1) 채널 폐쇄
내장 함수 close를 사용하여 채널을 닫습니다. 채널이 닫히면 더 이상 채널에 데이터를 쓸 수 없지만 채널에서 데이터를 읽을 수는 있습니다.
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 5)
intChan <- 10
intChan <- 20
close(intChan) //close
//关闭之后不能再向chan写入数据,但是可以读取
// intChan <- 30 //panic: send on closed channel
n1 := <- intChan
fmt.Println("n1=", n1) //n1= 10
}
(2) 채널 순회
채널은 범위 순회를 지원합니다. 두 가지 세부 사항에 주의하십시오.
- 트래버스할 때 채널이 닫히지 않으면 교착 상태 오류가 나타납니다.
- 순회 시 채널이 닫히면 데이터는 정상적으로 순회되며 순회가 완료된 후 순회가 종료됩니다.
package main
import (
"fmt"
)
func main(){
intChan := make(chan int, 5)
intChan <- 10
intChan <- 20
intChan <- 50
close(intChan) //close
//关闭之后不能再向chan写入数据,但是可以读取
// intChan <- 30 //panic: send on closed channel
n1 := <- intChan
fmt.Println("n1=", n1) //n1= 10
for v := range intChan {
fmt.Printf("value=%v\n", v)
}
}
7. 종합적인 예
- 요구 사항:
1-8000까지 계산해야 하는 숫자 중 소수는 무엇입니까?
고루틴 과 채널 에 대한 지식을 사용하여 완료- 분석 아이디어:
전통적인 방법은 주기를 사용하여 각 숫자가 소수인지 판단하는 것입니다[ok].
동시/병렬 방식을 사용하여 소수를 세는 작업을 여러(4) 개의 고루틴에 할당하여 완료하고 작업 완료 시간이 짧습니다.
package main
import (
"fmt"
"time"
)
//向intChan放入1-8000个数
func putNum(intChan chan int){
for i := 1; i <= 8000; i++ {
intChan <- i
}
//关闭chan
close(intChan)
}
//从intChan取出数据,并判断是否是素数,如果是,就放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool){
var flag bool
for {
time.Sleep(time.Millisecond * 10)
num, ok := <- intChan
if !ok {
//取不到数据了,就退出
break
}
flag = true //假设是素数
for i := 2; i < num; i++ {
if num % i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据,退出")
//这里我们还不能关闭primeChan
//向exitChan写入true
exitChan <- true
}
func main(){
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
//标识退出的管道
exitChan := make(chan bool, 4)
//开启一个协程,向intChan放入1-8000个数
go putNum(intChan)
//开启4个协程,从intChan取出数据,并判断是否是素数
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//主线程进行处理
go func(){
for i:= 0; i < 4; i++{
<-exitChan
}
//当我们从exitChan取出了4个结果,就可以放心的关闭primeChan
close(primeChan)
}()
//遍历primeChan,把结果取出
for {
res, ok := <- primeChan
if !ok {
break
}
//将结果取出
fmt.Printf("素数=%d\n", res)
}
}
결과:
8. 읽기 전용, 쓰기 전용 파이프 및 주의 사항
- 읽기 전용, 쓰기 전용 파이프:
- 지침
- 채널은 읽기 전용 또는 쓰기 전용으로 선언될 수 있습니다.
- 읽기 전용 및 쓰기 전용 사례
3) select를 사용하여 파이프라인에서 데이터를 가져오는 차단 문제를 해결합니다.
용도 선택:
package main
import (
"fmt"
"time"
)
func main(){
//使用select可以解决管道取数据的阻塞问题
//1. 定义一个管道 10 int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2. 定义一个管道 5 string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭,则会因阻塞导致deadlock
//可是我们在实际开发中,我们可能不好确定什么时候关闭管道
//办法:我们可以使用select方式解决
//label:
for {
select {
//注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
case v := <- intChan:
fmt.Printf("从intChan读取的数据=%d\n", v)
time.Sleep(time.Second)
case v := <- stringChan:
fmt.Printf("从stringChan读取的数据=%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("不玩了,都取不到了【程序员可以在这里加入自己的逻辑】\n")
time.Sleep(time.Second)
return
//break label
}
}
}
2 반사
2.1 개념
- Reflection은 변수의 타입(type), 카테고리(kind) 등 런타임 시 변수의 다양한 정보를 동적으로 얻을 수 있습니다.
- 구조체 변수인 경우 구조체 자체의 정보(구조체의 필드 및 메서드 포함)도 얻을 수 있습니다.
- 리플렉션을 통해 변수의 값을 수정하고 관련 메서드를 호출할 수 있습니다.
- 리플렉션을 사용하려면 가져오기(“리플렉트”)가 필요합니다.
5) 리플렉션 일반적인 애플리케이션 시나리오
2.2 리플렉션의 중요한 기능
- interface{}와 reflect.Value라는 변수는 상호 교환이 가능하며 실제 개발에서 자주 사용됩니다.
2.3 빠른 시작
(구조 유형, 인터페이스{}, reflect.Value)에 대한 리플렉션의 기본 작업을 시연하는 사례를 작성하세요.
package main
import (
"reflect"
"fmt"
)
type Student struct {
Name string
Age int
}
func reflectTest01(b interface{
}){
//通过反射获取到传入变量的type、kind值
//1. 先获取到reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)
//2. 获取到reflect.Value
rVal := reflect.ValueOf(b)
n2 := 2 + rVal.Int()
fmt.Println("n2=", n2)
fmt.Printf("rVal=%v rType=%T\n", rVal, rType)
//下面我们将rVal转成interface{}
iV := rVal.Interface()
//将interface{}通过断言转成需要的类型
num2 := iV.(int)
fmt.Println("num2=", num2)
}
//对结构体的反射
func reflectTest02(b interface{
}){
//通过反射获取到传入的变量的type、kind,值
//1. 先获取到reflect.Type
rType := reflect.TypeOf(b)
fmt.Println("rType=", rType)
//2. 获取到reflect.Value
rVal := reflect.ValueOf(b)
//下面我们将rVal转成interface{}
iV := rVal.Interface()
fmt.Printf("iv=%v iv type=%T\b", iV, iV)
//将interface{}通过断言转成需要的类型
//这里,我们使用类型断言【同学们可以使用switch的断言形式来更加灵活的判断】
stu, ok := iV.(Student)
if ok {
fmt.Printf("stu.Name=%v\n", stu.Name)
}
}
func main(){
//1. 基本数据类型 反射
var num int = 100
reflectTest01(num)
//2. 定义一个Student实例
stu := Student{
Name: "tom",
Age: 20,
}
reflectTest02(stu)
}
2.4 반영 내용 및 고려사항
- reflect.Value.Kind, 변수의 범주를 가져오고 상수를 반환합니다.
- Type과 Kind의 차이[type이 더 구체적임]
Type은 type, Kind는 category, Type과 Kind는 같거나 다를 수 있음 예: var num int = 10 The Type of num is int, Kind is a 예를 들어
var stu Student stu의 Type은 pkg1.Student이고 Kind는 struct입니다.
- 리플렉션을 통해 변수를 수정합니다.SetXxx 메소드를 사용하여 설정할 때 해당 포인터 유형을 통해 완료해야 전달된 변수의 값을 변경할 수 있습니다. Value.Elem() 메서드
- reflect.Value.Elem()을 어떻게 이해해야 합니까?
2.5 종합 사례
리플렉션을 사용하여 구조의 필드를 순회하고 구조의 메서드를 호출하고 구조 태그의 값을 가져옵니다.
package main
import (
"reflect"
"fmt"
)
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成绩"`
Sex string
}
//方法,返回两数的和
func (s Monster) GetSum(n1, n2 int) int {
return n1 + n2
}
//方法,接收四个值,给结构体赋值
func (s Monster) Set(name string, age int, score float32, sex string){
s.Name = name
s.Age = age
s.Score = score
s.Sex = sex
}
//方法,显示结构体的值
func (s Monster) Print(){
fmt.Println("----start----")
fmt.Println(s)
fmt.Println("----end----")
}
func TestStruct(a interface{
}){
//获取reflect.Type类型
typ := reflect.TypeOf(a)
//获取reflect.Value类型
val := reflect.ValueOf(a)
//获取到a对应的类别
kd := val.Kind()
//如果传入的不是struct,就退出
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}
//是结构体,获取该结构体有几个字段
num := val.NumField()
fmt.Printf("struct has %d fields\n", num)
for i := 0; i < num; i++ {
fmt.Printf("Field %d值为=%v\n", i, val.Field(i))
//获取到struct标签,注意需要通过reflect.Type来获取tag标签的值
tagVal := typ.Field(i).Tag.Get("json") //因为前面定义结构体用到了'json标签'
//如果该字段有tag标签就显示,否则就不显示
if tagVal != "" {
fmt.Printf("Field %d tag 为=%v\n", i, tagVal)
}
}
//获取到该结构体有多少个方法
numOfMethod := val.NumMethod()
fmt.Printf("struct has %d methods\n", numOfMethod)
//var params []reflect.Value
//方法的排序默认是按照函数名的排序(ASCII码)
val.Method(1).Call(nil) //获取到第二个【下标为1】方法,调用它 【传参为空】
//调用结构体的第1个方法 Method(0)
var params []reflect.Value //声明了 []reflect.Value()
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params) //传入的参数是[]reflect.Value,返回[]reflect.Value
fmt.Println("res=", res[0].Int()) //返回结果,返回的结果是[]reflect.Value
}
func main(){
var a Monster = Monster{
Name: "黄鼠狼精",
Age: 400,
Score: 30.9,
}
TestStruct(a)
}
3 네트워크 프로그래밍
3.1 개념 및 사전 지식
- TCP 소켓 프로그래밍은 네트워크 프로그래밍의 주류입니다. Tcp 소켓 프로그래밍이라고 하는 이유는 기본 계층이 Tcp/ip 프로토콜을 기반으로 하기 때문입니다.예: QQ 채팅
- b/s 구조의 http 프로그래밍의 경우 브라우저를 사용하여 서버에 액세스할 때 http 프로토콜을 사용하며 http의 맨 아래 계층은 여전히 tcp 소켓으로 구현됩니다. 예: Jingdong Mall [go 웹 개발 범주에 속함]
- 컴퓨터 간에 서로 통신하려면 네트워크 케이블, 네트워크 카드 또는 무선 네트워크 카드가 필요합니다.
4. 동의
5. 항구
- 0은 예약된 포트입니다.
- 1-1024는 잘 알려진 포트라고도 알려진 고정 포트(프로그래머가 사용하지 않음)입니다. 즉, 일부 프로그램에서 고정적으로 사용하며 일반 프로그래머는 사용하지 않습니다.
- 공통 포트: 22: SSH 원격 로그인 프로토콜 23: telnet 사용 21: ftp 사용
25: smtp 서비스 사용 80: iis 사용 7: echo 서비스- 1025-65535는 동적 포트입니다.
이 포트는 프로그래머가 사용할 수 있습니다.
注意:
-
컴퓨터(특히 서버)에서 가능한 한 적은 수의 포트를 엽니다.
-
포트는 하나의 프로그램에서만 모니터링할 수 있습니다.
-
netstat –an을 사용하면 이 시스템에서 수신 대기 중인 포트를 확인할 수 있습니다.
-
netstat –anb를 사용하여 수신 포트의 pid를 보고 작업 관리자와 함께 안전하지 않은 포트를 닫을 수 있습니다.
3.2 빠른 시작
- 서버 측 처리 흐름
- 포트 8888에서 수신 대기
- 클라이언트의 TCP 연결을 수신하고 클라이언트와 서버 간의 연결을 설정합니다.
- 링크 요청을 처리하기 위한 고루틴 생성(일반적으로 클라이언트는 링크를 통해 요청 패킷을 보냅니다.)
- 고객의 처리 흐름
- 서버와의 연결 설정
- 요청 데이터 전송[단말기], 서버에서 반환된 결과 데이터 수신
- 링크 닫기
①서버 기능 및 코드
기능:
- 포트 8888에서 수신하고 여러 클라이언트와 링크를 생성할 수 있는 서버 측 프로그램을 작성하십시오.
링크가 성공한 후 클라이언트는 데이터를 보낼 수 있고 서버는 데이터를 수락하여 터미널에 표시합니다. 텔넷을 사용하여 먼저 테스트하고, 그런 다음 테스트할 클라이언트 프로그램을 작성합니다.
암호:
package main
import (
"fmt"
"net"
)
func process(conn net.Conn) {
//循环接收客户端发送的数据
defer conn.Close()
for {
//创建一个新的切片
buf := make([]byte, 1024)
//1. 等待客户端通过conn发送消息
//2. 如果客户端没有write(发送消息),那么协程就阻塞在这里
fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
n, err := conn.Read(buf) //从conn中读取
if err != nil {
fmt.Printf("客户端退出 err=%v", err)
return
}
//3. 显示客户端给服务端发送的数据(打印在控制台上)
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听...")
//1. tcp表示使用的网络协议是tcp
//2. 0.0.0.0:8888表示在本地监听8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close() //延时关闭listen
//循环等待客户端来连接服务端
for {
//等待客户端连接
fmt.Println("等待客户端来连接...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)
} else {
fmt.Printf("Accept() success con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
}
//这里准备起一个协程,为客户端服务
go process(conn)
}
}
실행 후 효과:
服务器开始监听...
等待客户端来连接...
②클라이언트 기능 및 코드
- 서버의 포트 8888에 연결할 수 있는 클라이언트 프로그램을 작성하십시오.
- 클라이언트는 데이터의 단일 행을 보낸 다음 종료할 수 있습니다.
- 터미널을 통해 데이터를 입력(한 줄 입력하고 한 줄 보내기)하여 서버로 보낼 수 있음
- 프로그램을 종료하려면 터미널에 exit를 입력하십시오.
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "192.168.1.100:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 表示标准输入:【终端】
for {
//从终端读取一行用户输入,并发送给服务端
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
//功能二:当用户输入exit就退出
line = strings.Trim(line, "\r\n")
if line == "exit" {
fmt.Println("客户端退出...")
break
}
n, err := conn.Write([]byte(line))
if err != nil {
fmt.Println("conn Write err=", err)
}
fmt.Printf("客户端发送了 %d字节的数据\n", n)
}
}