《计算机网络编程》学到期中的知识点,主要是回顾计算机网络的一些基础知识和UNIX socket的API(C/C++)。

预备

知识点(不会考,但是应该作为预备知识):

  1. Linux虚拟机的安装
  2. Linux系统常用命令以及使用gcc进行c语言的编译。

网络基础与基本概念

计算机网络概念?

An interconnected collection of autonomous(自治的) computers。

LAN的物理拓扑结构?

总线型/环形/星形/。。。

OSI七层网络模型

IMG_2291.PNG
从上到下为:

  1. 应用层:常见的有HTTP,FTP,DNS等,为操作系统或网络应用程序提供访问网络服务的接口。
  2. 表示层
  3. 会话层
  4. 传输层:TCP协议,数据传输单位是数据段(segment);UDP协议,数据传输单位是数据报(datagarm)
  5. 网络层:IP协议,数据传输单位是数据包(packet)
  6. 数据链路层:数据传输单位是帧(frame)
  7. 物理层:数据传输单位是比特

字节顺序

  1. 主机字节序:不同的计算机体系结构可能使用不同的字节顺序(大端机和小端机)
    1. 大端机:和一般的表示一致
    2. 小端机:和一般的表示反过来
  2. 网络字节序:网络字节顺序应该与具体的CPU类型,操作系统无关——这样才能保证数据在不同主机间传输时能够被正确解释。

发送数据包时,将主机字节序转化为网络字节序;接收时,再将网络字节序转化为主机字节序
网络字节序采用大端的排序方式!

TCP/IP模型

image.png
从上至下,层层封装。从应用层的数据到传输层的TCP数据段网络层的IP数据包

UDP数据包格式

UDP提供无连接服务,缺乏可靠性支持。确认/超时重传/流量控制等功能都需要应用自己实现,所以它的头部相对TCP来说要简单很多。
image.png

1
2
3
4
5
6
struct udphdr {
u_int16_t source;//源端口
u_int16_t dest;//目的端口
u_int16_t len;//长度
u_int16_t check;//校验和
};

TCP数据包格式

TCP是面向连接,且提供了可靠性,流量控制和拥塞控制等功能。
image.png

1
2
3
4
5
6
7
8
9
10
11
struct tcphdr {
WORD SourPort;
WORD DestPort;
DWORD SeqNo;
DWORD AckNo;
BYTE HLen;
BYTE Flag;
WORD Window;
WORD ChkSum;
WORD UrgPtr;
}

连接过程

  1. 服务器被动打开,监听客户端等请求:调用socket,bind,listen函数。
  2. 客户端主动打开:调用connect函数。这时客户端向服务端发送一个SYN报文。
  3. 服务器确认客户的SYN,然后以单个报文向客户端发送SYN和对客户SYN的ACK。
  4. 客户确认服务器的SYN,即发送ACK报文。

    这个过程就是常说的三次握手。

image.png
关闭过程

  1. 某个进程首先调用close(客户端或服务端)。这时候主动关闭的这一端向另一端发送一个FIN报文。
  2. 被动关闭端确认发来的FIN报文,即向主动关闭端发送ACK报文。同时被动关闭端告诉应用程序关闭连接。

    使用文件结束标志传递给应用程序。

  3. 一段时间后,应用程序接收到文件结束标志后,调用close。这时被动关闭端会向主动关闭端发送一个FIN报文。

  4. 主动关闭方确认FIN报文。

    这个过程就是常说的四次挥手。

image.png

网络地址

  1. 物理地址(MAC地址)
  2. 逻辑地址(IP地址):分别为四类
  3. 端口号:包括周知端口0~1023,注册端口1024~49151,私有或动态端口49152~65535。
  4. 域名地址:使用DNS。

并发处理

并发包括真并发(并行,Parallelism)和假并发(分时,Concurrency)。
在客户机/服务器模型中,要实现客户端的并发,一般是不需要用户进行考虑的,因为现代的操作系统一般都能并发地执行客户程序。
所以我们需要研究的是服务端的并发

fork

1
pid_t fork(void)
  1. 调用时,控制权交给操作系统,创建新进程。
  2. 调用后,父子进程共享数据空间,代码空间,堆栈,调用前打开的所有文件描述符等(?)资源。

注意函数的返回值是进程的id:

  • 子进程中返回时,pid为0
  • 父进程中返回时,pid为子进程的pid
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include <stdlib.h>
    #include <stdio.h>

    int mul, sum;
    int main(void) {
    int i, pid;
    sum = 0;
    mul = 1;
    if((pid=fork()) > 0) {
    print("父进程执行");
    for(i=1; i<=5; i++) {
    printf("The value of i is %d\n",i);
    sum += i;
    }
    printf("The sum is %d\n",sum);
    }
    else if (pid == 0) {
    print("子进程执行");
    for (i=1; i<=5; i++) {
    printf("The value of i is %d\n",i);
    mul *= i;
    }
    printf("The multiplex is %d\n",mul);
    }

exec

调用exec函数后,系统会把原来的代码替换成新程序的代码,废弃原来的数据段和堆栈段,并为新程序分配新的数据段和堆栈段。唯一留下来的就是进程号。
对于系统而言,进程还是同一个进程,但是程序已经是另一个程序了

1
2
#include <unistd.h>
int execl(const char* path, const char* arg, ...);

协议的程序接口

TCP/IP协议存在于操作系统中,TCP/IP和应用程序之间接口应该是不精确指明的——不同OS中的接口细节不同:

  • UNIX中的套接字接口
  • Windows中的windows socket
  • MacOS/iOS/Andriod…

    学习UNIX的套接字接口。

UNIX使用一般的I/O来访问TCP/IP,即对一般的I/O调用进行了扩充,既可以用于I/O,也可以用于网络协议。

open

为输入或输出设备准备一个设备或文件。

close

终止使用以前已打开的设备或文件(释放资源)。

read

1
ssize_t read(int fd, void *buf, size_t nbytes);
  1. fd为文件描述符,socket也是文件描述符!
  2. buf为要接受数据缓冲区的地址
  3. nbytes为要读取的字节数

如果读取成功,返回读取到的字节数;读取失败,返回-1。

读取也是要写的,只是是写入而不是向外写。

write

1
ssize_t write(int fd, const void *buf, size_t nbytes);
  1. fd为文件描述符,socket也是文件描述符!
  2. buf为要写入数据缓冲区的地址
  3. nbytes为要写入的字节数

如果执行成功,返回写入的字节数;执行失败,返回-1。

套接字API

什么是套接字?

套接字的英文名是socket(插座),它是由本地的应用程序所创建,被操作系统所控制的接口。应用程序通过这个接口,可以使用传输层(TCP/UDP)提供的服务,跨网络与其他应用进程进行通信。
image.png

套接字的表示方法?

Unix的套接字采用通用的办法,定义支持一般网络通信的函数,TCP/IP通信是其中的一个特例,使用PF_INET表示TCP/IP族。
OS将文件描述符表用一个指针数组来表示,每个指针指向一个内部的数据结构。套接字和文件类似,每个活动的套接字都使用一个小整数来标识,称为套接字描述符
image.png

套接字的使用?

套接字分为主动套接字和被动套接字。

  • 主动套接字:如客户套接字
  • 被动套接字:如服务器套接字

使用套接字时需要指明端点地址(创建时不指定):

  1. TCP/IP需要指明端口号和IP地址
  2. TCP/IP协议族:PF_INET
  3. TCP/IP地址族:AF_INET

Linux内核源码中的#define AF_INET PF_INET可以看到二者是相同的,可以混用,但是他们字面意义不同,使用时尽量遵循规范:

  1. 建立socket时指定协议,即使用PF_XXX。如果是TCP/IP协议则指定为PF_INET:

    1
    s = socket(PF_INET, SOCKET_STREAM, 0);
  2. 设置地址时应该使用AF_XXX

    1
    2
    3
    4
    5
    struct sockaddr sin;
    menset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET; //设置地址族
    sin.sin_addr.s.addr = inet_addr(IP);
    sin.sin_port = htons(PORT);

    这里关系到sockaddrsockaddr_in到区别,他们的结构体如下:

    1
    2
    3
    4
    5
    struct sockaddr {
    u_char sa_len;
    u_short sa_family;
    char sa_data[14];
    }
    1
    2
    3
    4
    5
    6
    7
    struct sockaddr_in {
    u_char sin_len;
    u_short sin_family;
    u_short sin_port;
    struct in_addr sin_addr;
    char sin_zero[8];
    }

    两个结构体都占16字节,关系是:sockaddr_in是TCP/IP的地址定义,是sockaddr的特例。
    一般的套接字函数都考虑考虑到通用性,需要参数为sockaddr。我们使用时需要将自己定义的sockaddr_in强制类型转化为sockaddr,以通过类型检查。

API详解

先纵览C/S各自的代码。
服务端的代码示例如下:

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
47
48
49
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>

const char* IP = "0.0.0.0";
int PORT = 5000;
int BUF = 1024;

int main() {
//创建listen套接字
int s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(s < 0) {
printf("Fail to create socket.\n");
}else printf("Successfully Create socket.\n");

//将套接字和IP、端口绑定
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin)); //每个字节都用0填充
sin.sin_family = AF_INET; //使用IPv4地址
sin.sin_addr.s_addr = inet_addr(IP); //具体的IP地址
sin.sin_port = htons(PORT); //端口
bind(s, (struct sockaddr*)&sin, sizeof(sin));

//进入监听状态,等待用户发起请求
listen(s, 20);
printf("Start server in port %d\n", PORT);

//接收客户端请求
struct sockaddr_in cin;
socklen_t cin_size = sizeof(cin);
int c = accept(s, (struct sockaddr*)&cin, &clnt_size);
if(c < 0) {
printf("Connect error!\n");
} else printf("Connect success.\n");

//向客户端发送数据
char buf[] = "Hello World!";
write(c, buf, sizeof(buf));

//关闭套接字
close(c);
close(s);
printf("Close connection.");

return 0;
}

客户端的代码如下:
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
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/socket.h>
#include <string.h>

const char* IP = "127.0.0.1";
int SERVER_PORT = 5000;
int BUFLEN = 1024;

int main() {
// 使用 socket函数分配套接字
int s = socket(PF_INET, SOCK_STREAM, 0);
if(s < 0) {
printf("Can not create socket.\n");
} else printf("Socket created.\n");

// sockaddr_in结构体类型的变量,用于存储服务端信息
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin)); //归零
sin.sin_family = AF_INET;
// 这里写得太简单了,host还有可能是域名
inet_pton(AF_INET, IP, &sin.sin_addr.s_addr); //为什么?
sin.sin_port = htons(SERVER_PORT); //不要忘了转换网络字节序

// 选择本地端口号,使用connect函数即可
if(connect(s, (struct sockaddr*)&sin, sizeof(sin))<0) {
printf("Connect error.\n");
} else {
printf("Connect OK!\n");
}

// 应用级协议的通信,这里只是接收数据
char buffer[BUFLEN];
memset(buffer, 0, BUFLEN);
if(read(s, buffer, BUFLEN)<0) {
printf("Read error.\n");
} else {
printf("Receive data:\n%s", buffer);
}

close(s);
return 0;
}

服务端发送数据,客户端接受数据。

socket

1
int socket(int domain, int type, int protocal);
  1. domain:域类型,即使用的协议族/协议栈。TCP/IP为PF_INET
  2. type:需要的服务类型
    1. SOCK_STREAM: 流服务,TCP。
    2. SOCK_DGRAM: 数据报服务,UDP。
  3. protocol:一般取0即可。

connect

1
int connect(int sockfd, (struct sockaddr*) server_addr, int sockaddr_len);
  1. sockfd:套接字描述符,即调用socket返回的int。
  2. server_addr:指明远程端点的结构体
  3. sockaddr_len:地址长度(结构体大小)

bind

1
int bind(int sockfd, (struct sockaddr*) my_addr, int addr_len);

为被动套接字指明一个本地端点地址。参数含义和connect一样。

listen

1
int listen(int sockfd, int queue_size);
  1. sockfd:套接字描述符
  2. queue_size:套接字使用的队列长度,用于指定在请求队列中允许的最大请求数。

accept

1
int accept(int sockfd, void* addr, int* addr_len);
  1. sockfd:指明正在监听的套接字。
  2. addr:存储请求连接的主机地址。
  3. addr_len:地址的长度。

close

撤销套接字:

  1. 如果只有一个进程使用,立即终止连接并撤销套接字。
  2. 如果多个进程共享该套接字,则将引用数-1,如监听套接字。

其他

基于数据流(TCP)发送和接收的函数sendrecv,在UNIX中,可以使用readwrite替代。因为它们都调用内核的sosend实现。
基于数据报(UDP)发送和接收的函数需要另外掌握。

sendto

1
int sendto(int sockfd, const void* data, int data_len, unsigned int flags, (struct sockaddr*) remaddr, int remaddrlen);
  1. data:要发送的数据指针。
  2. data_len:数据的长度。
  3. flags:一般为0。
  4. remaddr:远端的端点地址。
  5. remaddr_len:地址长度。

recvfrom

1
int recvfrom(int sockfd, void* buf, int buf_len, unsigned int flags, (struct sockaddr*) from, int fromlen);

参数的含义类似。

网络字节顺序

网络字节顺序的最高位字节在前,socketaddr_in要求参数按照网络字节顺序存储,所以需要经常进行主机字节顺序到网络字节顺序的转换。

  1. htons:将一个短整数从本地字节顺序转换为网络字节顺序。
  2. ntohs:将一个短整数从网络字节顺序转换为本地字节顺序。

    不转换程序也不会报错,但是设置的端点地址就和我们预想的不一样了,这一点需要警惕!