1.1 理解网络和套接字
套接字
套接字(socket)是计算机网络中进程间通信的一种机制,它将进程间通信抽象为一个端点,该端点由一个IP地址和一个端口号来标识。
套接字是网络数据传输用的软件设备。网络编程又称为套接字编程。
编写" Hello world! " 服 务 器 端
网络编程中接受连接请求的套接字创建过程可整理如下。
- 第一步:调用s o c k e t 函数创建套接字。
- 第二步:调用b i n d 函数分配E地址和端口号。
- 第三步:调用l i s t e n 函数转为可接收请求状态。
- 第四步:调用a c c e p t 函数受理连接请求。
hello_server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| package main
import (
"log"
"net"
"os"
)
func main() {
//检查参数
if len(os.Args) != 2 {
log.Fatalf("Usage: %s <port>\n", os.Args[0])
}
//设置监听端口和信息
message := "Hello world!"
port := os.Args[1]
//当你调用 net.Listen("tcp", ":"+port) 时,Go 会执行以下操作:
//1.创建一个 TCP 套接字。
//2.将套接字绑定到指定的 IP 地址和端口号。
//3.将套接字转为可接收连接状态
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("socket() error: %v", err)
}
//延迟关闭监听
defer listener.Close()
//循环监听
for {
//4.Accept()接收连接
conn, err := listener.Accept()
if err != nil {
log.Fatalf("accept() error: %v", err)
}
//5.write()发送信息
_, err = conn.Write([]byte(message))
if err != nil {
log.Fatalf("write() error: %v", err)
}
conn.Close()
}
}
|
client
创建套接字,但此时套接字并不马上分为服务器端和客户端。如果紧接着调用bind和listen函数,将成为服务器端套接字;如果调用connect函数将成为客户端套接字。
hello_client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| package main
import (
"fmt"
"io"
"log"
"net"
"os"
)
func main() {
if len(os.Args) != 3 {
log.Fatalf("Usage: %s <IP> <port>\n", os.Args[0])
}
ip := os.Args[1]
port := os.Args[2]
address := fmt.Sprintf("%s:%s", ip, port)
//1.创建一个 TCP 套接字。
//2.调用 Connect() 连接到指定的 IP 地址和端口号。
conn, err := net.Dial("tcp", address)
if err != nil {
log.Fatalf("Failed to connect to server: %v", err)
}
defer conn.Close()
//net.Conn 对象来读取服务器发送的消息
message, err := io.ReadAll(conn)
if err != nil {
log.Fatalf("Failed to read from server: %v", err)
}
fmt.Printf("Message from server: %s\n", message)
}
|
运行
1
2
| go build -o ch1/hello_server ch1/hello_server.go
./ch1/hello_server 8080
|
1
2
3
4
5
| go build -o ch1/hello_client ch1/hello_client/hello_client.go
./ch1/hello_client 127.0.0.1 8080
Message from server: Hello world!
|
1.2 文件操作
打开文件
首先介绍打开文件以读写数据的函数。 调用此函数时需传递两个参数:第一个参数是打开的目标文件名及路径信息,第二个参数是文件打开模式(文件特性信息)。
1
2
3
4
5
6
7
| #include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char * path ,int flag)
//成功时返回文件描述符事失败时返回1
//path 文件名字符串地址
//flag 文件打开模式
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
// OpenFile 是一个通用的文件打开函数,它接受三个参数:文件名、打开文件的标志和文件权限。
// 它返回一个 *os.File 对象和一个 error 对象。如果打开文件失败,它会返回一个非 nil 的 error 对象。
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
// 记录打开文件的操作
testlog.Open(name)
// 使用 openFileNolog 函数打开文件,这个函数不记录日志
f, err := openFileNolog(name, flag, perm)
// 如果打开文件失败,返回错误
if err != nil {
return nil, err
}
// 检查打开文件的标志是否包含 O_APPEND,如果包含,设置 f.appendMode 为 true
f.appendMode = flag&O_APPEND != 0
// 返回打开的文件
return f, nil
}
|
关闭文件
1
2
3
4
| #include <unistd.h>
int close(int fd);
//成功时返回Q,失败时返回-1。
//fd:需要关闭的文件或套接字的文件描述符
|
1
2
3
4
5
6
7
8
9
10
| // Close closes the File, rendering it unusable for I/O.
// On files that support SetDeadline, any pending I/O operations will
// be canceled and return immediately with an ErrClosed error.
// Close will return an error if it has already been called.
func (f *File) Close() error {
if f == nil {
return ErrInvalid
}
return f.file.close()
}
|
将数据写入文件
1
2
3
4
5
6
| #include <unistd.h>
ssize_t write(int fd, const void * buf, size_t nbytes);
//成功时返回写入的字节数,失败时返回-1。
//fd:数据传输对象的文件描述符
//buf 保存数据的缓冲地址
//nbytes:要传输的字节数
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
// write 是 File 结构体的一个方法,它接受一个字节切片 b 作为参数,
// 尝试将这个字节切片写入到文件中。
func (f *File) write(b []byte) (n int, err error) {
// f.pfd.Write(b) 调用 pfd(代表平台依赖的文件描述符)的 Write 方法,
// 尝试将 b 写入到文件。这个方法返回写入的字节数和一个错误(如果有的话)。
n, err = f.pfd.Write(b)
// runtime.KeepAlive(f) 是一个用于防止 f 被垃圾回收的调用。
// 在某些情况下,如果 f 在 f.pfd.Write(b) 调用之后没有被再次使用,
// Go 的垃圾回收器可能会在 Write 调用还在进行时就回收 f。
// runtime.KeepAlive(f) 确保 f 在 Write 调用完成之前不会被垃圾回收。
runtime.KeepAlive(f)
// 返回写入的字节数和错误(如果有的话)
return n, err
}
|
代码示例改写
low_open.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| package main
import (
"log"
"os"
)
func main() {
buf := []byte("Let's go!\n")
//打开文件,如果不存在则创建,如果存在则清空,权限为 0644
f, err := os.OpenFile("data.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
log.Fatalf("open() error: %v", err)
}
defer f.Close()
//输出文件描述符
log.Printf("file descriptor: %d \n", f.Fd())
//写入文件
_, err = f.Write(buf)
if err != nil {
log.Fatalf("write() error: %v", err)
}
}
|
运行代码
1
2
3
| go build -o ch1/low_open ch1/lowOpen/low_open.go
./ch1/low_open
file descriptor: 3
|
读取数据
1
2
3
4
5
6
| #include <unistd.h>
ssize_t read(int fdJ void * buf, size_t nbytes);
//'成功时返回接收的字节数(但遇到文件结尾则返回θ),失败时返回10
//fd 显示数据接收对象的文件描述符。
//buf 要保存接收数据的缓冲地址值。
//nbytes 要接收数据的最大字节数。
|
示例代码改写
low_read.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| package main
import (
"log"
"os"
)
func main() {
//只读打开文件
f, err := os.Open("data.txt")
if err != nil {
log.Fatalf("open() error: %v", err)
}
defer f.Close()
//输出文件描述符
log.Printf("file descriptor: %d \n", f.Fd())
//读取文件
buf := make([]byte, 1024)
n, err := f.Read(buf)
if err != nil {
log.Fatalf("read() error: %v", err)
}
log.Printf("file data: %s", buf[:n])
}
|
运行代码
1
2
3
4
5
| go build -o ch1/low_read ch1/lowRead/low_read.go
./ch1/low_read
2023/11/10 16:45:54 file descriptor: 3
2023/11/10 16:45:54 file data: Let's go!
|
文件描述符与套接字
原文c代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| #include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main()
{
int fd1, fd2, fd3;
//创建一个文件和两个套接字
fd1 = socket(PF_INET, SOCK_STREAM, 0);
fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC);
fd3 = socket(PF_INET, SOCK_DGRAM, 0);
//输出之前创建的文件描述符的整数值
printf("file descriptor 1: %d\n", fd1);
printf("file descriptor 2: %d\n", fd2);
printf("file descriptor 3: %d\n", fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
|
改写后
fd_seri.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import (
"fmt"
"os"
"syscall"
)
func main() {
// 使用 syscall.Socket 函数创建一个 TCP 套接字,返回的是文件描述符 fd1
fd1, _ := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0)
// 使用 os.OpenFile 函数创建一个文件 "test.dat",返回的是 *os.File 类型的 fd2
fd2, _ := os.OpenFile("test.dat", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
// 使用 syscall.Socket 函数创建一个 UDP 套接字,返回的是文件描述符 fd3
fd3, _ := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
// 使用 fmt.Printf 函数打印出三个文件描述符的值
// 注意,对于 *os.File 类型的 fd2,我们使用了 Fd 方法来获取其底层的文件描述符
fmt.Printf("file descriptor 1: %d\n", fd1)
fmt.Printf("file descriptor 2: %d\n", fd2.Fd())
fmt.Printf("file descriptor 3: %d\n", fd3)
// 使用 Close 方法和 syscall.Close 函数关闭了这三个文件描述符,以释放系统资源
fd2.Close()
syscall.Close(fd1)
syscall.Close(fd3)
}
|
运行
1
2
3
4
5
| go build -o ch1/fd_seri ch1/fdSeri/fd_seri.go
./ch1/fd_seri
file descriptor 1: 3
file descriptor 2: 4
file descriptor 3: 5
|
1.3 基于Windows平台的实现
略
1.4 基于Windows的套接字相关函数及示例
1.5 习题
套接字在网络编程中的作用是什么?为何称它为套接字?
套接字(socket)是网络编程中的抽象概念,它提供了一种机制,使得不同计算机之间可以进行通信和数据交换。套接字可以看作是网络通信的端点,它包含了通信所需的各种信息,如IP地址、端口号、协议等。套接字的名称来源于插座(socket),类比插座连接电器,套接字连接网络。通过套接字,计算机可以在网络上进行数据的发送和接收,实现网络通信的功能。
在服务器端创建套接字后,会依次调用listen函数和accept 函数。请比较并说明二者作用。
listen函数用于将套接字标记为被动套接字,即用于接受客户端的连接请求。它告诉操作系统该套接字将用于接受传入的连接,而不是发起连接。在调用listen函数后,套接字将进入监听状态,等待客户端的连接请求。
accept函数用于从处于监听状态的套接字中接受一个连接。当客户端尝试连接到服务器时,服务器调用accept函数来接受这个连接,并创建一个新的套接字来与客户端进行通信。这个新的套接字可以用于与该客户端进行数据交换,而原始的监听套接字则继续等待其他客户端的连接请求。accept函数的返回值是一个新的套接字,通过它可以进行与客户端的通信。
Linux中,对套接字数据进行I/O时可以直接使用文件I/O 相关函数;而在Windows中则不可以。原因为何?
这是因为在Linux中,套接字被视为一种文件描述符,因此可以使用文件I/O相关函数(如read和write)来进行I/O操作。而在Windows中,套接字和文件描述符是不同的概念,Windows采用了不同的I/O模型,因此不能直接使用文件I/O相关函数来对套接字数据进行I/O操作。在Windows中,需要使用特定的套接字I/O函数(如recv和send)来进行套接字数据的读写操作。
创建套接字后一般会给它分配地址,为什么?为了完成地址分配需要调用哪个函数
创建套接字后需要给它分配地址,这是为了让其他主机能够找到并与该套接字进行通信。在网络编程中,这个地址通常是IP地址和端口号的组合。
为了完成地址分配,需要调用bind函数。bind函数将一个本地地址(IP地址和端口号)分配给套接字,使得其他主机可以通过这个地址与该套接字进行通信。
Linux中的文件描述符与Windows的句柄实际上非常类似。请以套接字为对象说明它们的含义。
文件描述符和Windows的句柄在套接字的上下文中具有类似的含义。它们都是用来标识和引用套接字的抽象概念。
在Linux中,套接字也被视为一种文件描述符,因此可以使用类似于文件I/O的操作来进行套接字的读写等操作。
在Windows中,套接字使用句柄来进行引用和操作,句柄是一种抽象的引用类型,可以用来标识和操作套接字。
因此,无论是文件描述符还是句柄,它们都是用来引用和操作套接字这种抽象对象的标识符。
底层文件I/O函数与ANSI 标准定义的文件I/O函数之间有何区别?
底层文件I/O函数是直接调用操作系统提供的文件操作接口,如open、read、write等,它们提供了对文件的低级别访问,可以更加灵活地控制文件的读写操作。
而ANSI标准定义的文件I/O函数则是标准C库中提供的一组文件操作函数,如fopen、fread、fwrite等,它们提供了更加抽象和便捷的文件操作接口,使得跨平台开发更加方便,并且提供了一些缓冲和错误处理的功能。
因此,底层文件I/O函数更加接近操作系统提供的文件操作接口,而ANSI标准定义的文件I/O函数则提供了更加便捷和跨平台的文件操作接口。
参考本书给出的示例low_open.c 和low_read.c ,分别利用底层文件I/O 和ANSI标准I/O 编写文件复制程序。可任意指定复制程序的使用方法。