《TCP IP网络编程》第一章

        2023.6.28 正式开始学习网络编程。 每一章每一节的笔记都会记录在博客中以便复习。


第1章

1.1理解网络编程和套接字

        网络编程又叫套接字编程。所谓网络编程,就是编写程序使两台连网的计算机相互交换数据。 为什么叫套接字编程? 我们平常将插头插入插座上就能从电网中获取电力,同样的道理,为了与远程计算机进行数据传输,需要连接到因特网,而编程中的“套接字”就是用来链接网络的工具。

        服务器端创建的套接字又叫服务器端套接字或者监听套接字。 其请求连接的套接字创建过程分为四步:

  1. 调用socket函数创建套接字
  2. 调用bind函数分配IP地址和端口号
  3. 调用listen函数转为可接受请求状态
  4. 调用accept函数受理连接请求

        请求连接的客户端套接字创建过程如下两步:

  1. 调用socket函数创建套接字
  2. 调用connect函数向服务器端发送连接请求

        值得注意的是:创建完套接字,并不会马上区分为服务器端或客户端。 如果接下来紧接着调用bind、listen等函数则成为服务器端套接字;如果调用connect函数则称为客户端套接字。

        接下来在linux环境中编译并执行上述两个实例:hello_server.c文件和hello_client.c文件。

        分别对客户端和服务端程序进行编译:    

gcc hello_server.c -o hserver
gcc hello_client.c -o hclient

        该命令中的-o是用来指定可执行文件名的可选参数,因此,编译后将生成可执行文件hserver和hclient。

            运行:

./hserver 9190
./hclient 127.0.0.1 9190

        运行的时候,首先在 9190 端口启动服务,然后 heserver 就会一直等待客户端进行响应,当客户端监听位于本地的 IP 为 127.0.0.1 的地址的9190端口时,客户端就会收到服务端的回应,输出`Hello World!`。

        ps:执行过程中输入的127.0.0.1是本地计算机的IP地址。 如果在同一台计算机中同时运行服务器端和客户端,将采用这种连接方式。 但如果服务器端和客户端在不同计算机中运行,则应采用服务器端所在计算机的IP地址。


1.2基于Linux的文件操作

                                                  底层访问和文件描述符

        在linux中,socket也是文件的一种,因此在网络数据传输过程中可以使用文件I/O相关的函数。而Windows需要区分socket和文件,因此在Windows中需要调用特殊的数据传输相关函数。

        每当生成文件或套接字,操作系统将返回分配给他们的整数,(即文件描述符),这个整数将成为程序员与操作系统之间良好沟通的渠道,实际上,文件描述符是为了方便称呼系统创建的文件或套接字而赋予的数。 文件描述符也成为文件句柄,“句柄”是Windows中的术语,Linux平台则使用“描述符”。

                                                        打开文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);
/*
成功时返回文件描述符,失败时返回-1
path : 文件名的字符串地址
flag : 文件打开模式信息
*/

                                                         关闭文件

#include <unistd.h>
int close(int fd);
/*
成功时返回 0 ,失败时返回 -1
fd : 需要关闭的文件或套接字的文件描述符
*/

        若调用此函数同时传递文件描述符参数,则关闭(终止)响应文件。另外需要注意的是,此函数不仅可以关闭文件,还可以关闭套接字。再次证明了「Linux 操作系统不区分文件与套接字」的特点。 

                                                    将数据写入文件

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
/*
成功时返回写入的字节数 ,失败时返回 -1
fd : 显示数据传输对象的文件描述符
buf : 保存要传输数据的缓冲值地址
nbytes : 要传输数据的字节数
*/

        在此函数的定义中,size_t 是通过 typedef 声明的 unsigned int 类型。对 ssize_t 来说,ssize_t 前面多加的 s 代表 signed ,即 ssize_t 是通过 typedef 声明的 signed int 类型。

创建新文件并保存数据:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void error_handling(char *message);

int main()
{
    int fd;
    char buf[] = "Let's go!\n";
    // O_CREAT | O_WRONLY | O_TRUNC 是文件打开模式,将创建新文件,并且只能写。如存在 data.txt 文件,则清空文件中的全部数据。
    fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC);
    if (fd == -1)
        error_handling("open() error!");
    printf("file descriptor: %d \n", fd);
    // 向对应 fd 中保存的文件描述符的文件传输 buf 中保存的数据。
    if (write(fd, buf, sizeof(buf)) == -1)
        error_handling("write() error!");
    close(fd);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

         运行后会生成一个data.txt的文件,里面有Let's go!

                                                        读取文件中的数据

        与之前的write()函数相对应,read()函数用来输入(接收)数据:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
/*
成功时返回接收的字节数(但遇到文件结尾则返回 0),失败时返回 -1
fd : 显示数据接收对象的文件描述符
buf : 要保存接收的数据的缓冲地址值。
nbytes : 要接收数据的最大字节数
*/

        下面代码通过 read() 函数读取 data.txt 中保存的数据:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 100
void error_handling(char *message);

int main()
{
    int fd;
    char buf[BUF_SIZE];

    fd = open("data.txt", O_RDONLY);
    if (fd == -1)
        error_handling("open() error!");
    printf("file descriptor: %d \n", fd);

    if (read(fd, buf, sizeof(buf)) == -1)
        error_handling("read() error!");
    printf("file data: %s", buf);
    close(fd);
    return 0;
}
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

实验结果: 

         low.open.c创建文件并保存文件数据,运行之后返回文件描述符3。用cat命令输出data.txt的文件内容,可以确认确实向文件传输了数据。

        low.read.c程序则通过read函数读取了data.txt中保存的数据。运行之后打印文件描述符以及文件的内容。 

        fd_seri.c 程序同时创建文件和套接字:

    fd1 = socket(PF_INET, SOCK_STREAM, 0);
    fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC);
    fd3 = socket(PF_INET, SOCK_DGRAM, 0);

然后分别打印其文件描述符得到3、4、5,描述符从3开始以由小到大的顺序编号,因为0、1、2是分配给标注能I/O的描述符。如下图:

​​​​​​​

1.5 习题

1.套接字在网络编程中的作用是什么?为何称它为套接字?

        套接字是一种用于在网络上进行通信的编程接口。套接字允许不同计算机上的进程通过网络进行数据传输。它提供了一种机制,使得计算机之间可以建立连接、发送和接收数据。套接字使得应用程序能够利用网络进行通信,实现客户端和服务器之间的数据交换。

        它被称为套接字,是因为它类比于电话通信中的插座,作为网络通信的端点连接点。

2.在服务器端创建套接字后,会依次调用listen函数 和accept函数。请比较并说明二者作用。

        listen函数用于将套接字设置为被动监听模式,以接受客户端的连接请求。

        accept函数用于接受客户端的连接请求,并创建一个新的套接字来处理与该客户端的通信。

3.Linux中,对套接字数据进行I/O时可以直接使用文件I/O相关函数;而在Windows中则不可以,原因为何?

        这是因为在Unix-like系统中,包括Linux,一切皆文件的思想被广泛采用,将各种资源(包括套接字)都抽象为文件描述符的形式,统一了数据的读写接口。

        然而,在Windows操作系统中,套接字不被视为文件描述符,因此不能直接使用文件I/O相关函数进行数据的读写操作。

4.创建套接字后一般会给它分配地址, 为什么?为了完成地址分配需要调用哪个函数?

        在网络编程中,创建套接字后需要给它分配地址,主要是为了使其他计算机能够找到并与该套接字建立连接。通过分配地址,可以指定套接字的IP地址和端口号,从而唯一标识该套接字在网络中的位置。

        为了完成地址分配,需要调用bind函数。bind函数用于将套接字与特定的IP地址和端口号进行绑定。

猜你喜欢

转载自blog.csdn.net/m0_61028090/article/details/131446126