一、理解网络编程和嵌套字

1.1 理解网络编程和嵌套字

服务端嵌套字创建过程

  1. socket() 创建嵌套字
  2. bind() 分配 IP 与端口号
  3. listen() 转变为可接受请求状态
  4. accept() 受理连接请求
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
50
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handing(char *message);

int main(int argc, char *argv[]){
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;

char message[] = "Hello world";
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1){
error_handing("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) ==-1){
error_handing("bind() error");
}
if(listen(serv_sock, 5)==-1){
error_handing("listen() error");
}
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1){
error_handing("accept() error");
}
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}

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

客户端嵌套字创建过程

  1. socket() 创建嵌套字
  2. connect() 发送连接请求
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(int argc, char *argv[]){
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1){
perror("socket() error");
exit(1);
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) ==-1){
perror("connect() error");
exit(1);
}
str_len = read(sock, message, sizeof(message)-1);
if(str_len == -1){
perror("read() error");
exit(1);
}
printf("Message from server : %s \n", message);
close(sock);
return 0;
}

1.2 基于 Linux 的文件操作

在 Linux 系统中,socket 也是一种文件,可以使用文件 I/O 的相关函数,需要理解文件描述符的概念。

文件描述符:是文件系统分配给文件或者套接字的整数,方便称呼操或者代指作系统创建的文件或者套接字,这个整数有利于程序员编程。

开打文件

1
2
3
4
5
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int open(const char *path, int flag)
  • 成功时返回文件描述符,失败返回 -1
  • path:表示文件地址
  • flag:文件打开模式信息
O_CREAT 没有则创建文件
O_TRUNC 删除现有数据
O_APPEND 维持现有数据,追加到文件末尾
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开

关闭文件

1
2
3
#include<unistd.h>

int colse(int fd)
  • 成功时返回0,失败返回 -1
  • fd:表示文件描述符

写入文件

1
2
3
#include<unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);
  • 成功时返回写入的字节数,失败返回 -1
  • fd:表示文件描述符
  • buf:要写入数据的指针
  • nbytes:传输数据字节数
  • ssize_t,size_t:分别是 typedef 声明的 int , unsigned int 类型,操作系统定义的数据类型会添加后缀 _t
1
2
3
#include<unistd.h>

ssize_t send(int __fd, void *__buf, size_t __n, int __flags);
  • 成功时返回接受的字节数,失败返回 -1
  • fd:表示文件描述符
  • buf:要保存数据的指针
  • n:传输数据字节数
  • flags
    • 0send 与 write 行为完全相同,一般设置为 0
    • MSG_DONTWAIT:非阻塞操作一次,即使 socket 是阻塞模式,本次调用也不会阻塞。
    • MSG_NOSIGNAL(仅 send):当对端已关闭连接时,不产生 SIGPIPE 信号,而是返回错误 EPIPE,避免程序崩溃。
    • MSG_MORE:告诉内核本次发送的数据后面还会紧跟更多小包,可以延迟合并发送。
    • MSG_OOB:发送带外数据(紧急数据)

读取文件

1
2
3
#include<unistd.h>

ssize_t read(int fd, void *buf, size_t nbytes);
  • 成功时返回接受的字节数,失败返回 -1
  • fd:表示文件描述符
  • buf:要保存数据的指针
  • nbytes:传输数据字节数
1
2
3
#include<unistd.h>

ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);
  • 成功时返回接受的字节数,失败返回 -1
  • fd:表示文件描述符
  • buf:要保存数据的指针
  • n:传输数据字节数
  • flags
    • 0recv 与 read 行为完全相同,一般设置为 0
    • MSG_PEEK:把数据“偷看”到缓冲区,但不把数据从接收缓冲区移除,下次 recv 还能再收一遍。
    • MSG_DONTWAIT:非阻塞操作一次,即使 socket 是阻塞模式,本次调用也不会阻塞。
    • MSG_OOB:接收带外数据(紧急数据)。
    • MSG_NOSIGNAL(仅 send):当对端已关闭连接时,不产生 SIGPIPE 信号,而是返回错误 EPIPE,避免程序崩溃。
    • MSG_WAITALL:仅当发生以下事件之一时,接收请求才会完成:
      • 阻塞直到收到指定长度的数据
      • 连接已关闭。
      • 该请求已被取消或发生错误。

套接字类型与协议设置

2.1 套接字协议及其数据传输特性

1
2
3
#include<sys/socket.h>

int socket(int domain, int type, int protocol);
  • domain:套接字中使用的协议族(Prototol Family)信息
  • type: 套接字数据传输类型信息
  • protocol 计算机间通信使用的协议信息

协议族 domain

名称 协议族
PF_INET IPv4互联网协议族
PF_INET6 IPv6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell协议族

套接字类型 Type

  • SOCK_STREAM:面向连接的套接字(TCP)
    • 传输过程中数据不会消失
    • 按序传输数据
    • 传输的数据不存在数据边界
  • SOCK_DGRAM:面向连接的套接字(UDP)
    • 强调快速传输而非传输顺序
    • 传输的数据可能丢失也可能损毁
    • 传输的数据有数据边界
    • 限制每次传输的数据大小

protocol

PF_INET 指的是 IPv4 网络协议族,在该网络协议族下面向连接的套接字只有 TCP,面向连接的套接字只有 UDP,默认写 0 即可

2.2 TCP套接字示例

TCP套接字:传输的数据不存在边界,在客户端中可以分多次调用 read 函数以接收服务器端发送的数据,也可以只调用一次 read 函数接受全部函数。

服务端

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
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handing(char *message);

int main(int argc, char *argv[]){
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;

char message[] = "Hello world!";
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1){
error_handing("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) ==-1){
error_handing("bind() error");
}
if(listen(serv_sock, 5)==-1){
error_handing("listen() error");
}
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1){
error_handing("accept() error");
}
// write(clnt_sock, message, sizeof(message));
int idx = 0;
while(write(clnt_sock, &message[idx++], 1)){
if(idx >= sizeof(message)){
break;
}
}
close(clnt_sock);
close(serv_sock);
return 0;
}

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

客户端

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handing(char *message);

int main(int argc, char *argv[]){
int sock;
struct sockaddr_in serv_addr;
char message[30];
int idx = 0;
int str_len = 0, read_len = 0;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1){
error_handing("socket() error");
}
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) ==-1){
error_handing("connect() error");
}
printf("sleep 1s\n");
sleep(1);
while(read_len = read(sock, &message[idx++], 1)){
if(read_len == -1){
error_handing("read() error");
}
str_len += read_len;
}
printf("Message from server : %s \n", message);
printf("Function read call count: %d\n", str_len);
close(sock);
return 0;
}

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

地址族与数据序列

3.1 套接字的IP地址和端口号

IP地址用于区分计算机,一个端口号对应一个主机内的一个套接字进程

  • A类地址的首字节范围:0~127
  • B类地址的首字节范围:128~191
  • C类地址的首字节范围:192~223

3.2 地址信息的表示

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family)
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8] //不使用
};

struct in_addr{
in_addr_t s_addr; //32位IPv4地址
};
  • 成员 sin_family

    地址族(Address Family) 含义
    AF_INET IPv4网络协议中使用的地址族
    AF_INET6 IPv6网络协议中使用的地址族
    AF_LOCAL 本地通信中采用的UNIX协议的地址族
  • 成员 sin_port

    该成员是以网络字节序保存的 16 位端口号

  • 成员 sin_addr

    该成员是以网络字节序保存的 32 位 IP

  • 成员 sin_zero 无特殊含义。只是为了使结构体 sockaddr_in 的大小与 sockaddr 结构体保持一致而插入的成员。必须填充为0

    1
    2
    3
    struct sockaddr_in serv_addr;
    if(bind(serv_addr, (struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
    error_handling("bind() error");

3.3 网络字节序与地址变换

CPU 向内存保存数据的方式有两种,意味着 CPU 解析数据的方式也分为两种,假设要在内存中存储整数 0x12 0x34 0x56 0x78。 - 大端序(Bid Endian): 高位字节存放到低位地址-0x12 0x34 0x56 0x78 - 小端序(Little Endian): 高位字节存放到高位地址-0x78 0x56 0x34 0x12(Inter CPU)

每种 CPU 的数据保存方式可能不同。因此,代表 CPU 数据保存方式的主机字节序(Host Byte Order在不同CPU中也各不相同,这会小端序的主机向大序端的主机发送数据可能会导致接收方收到了错误的数据。为了解决上述问题,在通过网络传输数据时约定了统一方式,这种约定称为网络字节序,统一为大端序。 ### 3.3.1 字节序转换

  • unsigned short htons(unsigned short);
  • unsigned short ntohs(unsigned short);
  • unsigned long htonl(unsigned long);
  • unsigned long ntohl(unsigned long);

htons 中 的 h 代表主机(host)字节序,n 代表网络(network)字节序,s 指 short,l 指 long(==Linux中long类型占用4字节==)。

htons,可以解释为:把 short 类型数据从主机字节序转化为网络字节序。 ntohs,可以解释为:把short型数据从网络字节序转化为主机字节序。

3.4 网络地址的初始化与分配

sockaddr_in 中保存地址信息的成员为 32 位整数型,因此为了分配 IP 地址,需要将字符串类型,转换为 4 字节整数型数据。

inet_addr

将字符串类型,转换为 4 字节整数型数据。

1
2
3
4
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string);
//成功时返回32位大端序整数型值
//失败时返回INADDR_NONE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char* argv[]){
char addr1[] = "1.2.3.4";
char addr2[] = "1.2.3.256";

unsigned long conv_addr = inet_addr(addr1);
if(conv_addr == INADDR_NONE){
printf("inet_addr error\n");
}
else{
printf("conv_addr:%lx\n", conv_addr);
}
conv_addr = inet_addr(addr2);
if(conv_addr == INADDR_NONE){
printf("inet_addr error\n");
}
else{
printf("conv_addr:%lx\n", conv_addr);
}
return 0;
}

inet_aton

inet_aton 函数与 inet_addr 函数在功能上完全相同,也将字符串形式 IP 地址信息带入sockaddr_in 结构体中声明的 in_addr 结构体变量。而 inet_aton 函数则不需要此过程,因为它会在转换后,再把结果填入该结构体变量。

1
2
#include <arpa/inet.h>
int inet_aton(const char* string, struct in_addr* addr);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char* argv[]){
char addr[] = "127.232.124.79";
struct sockaddr_in addr_inet;
if(!inet_aton(addr, &addr_inet.sin_addr)){
perror("inet_aton error\n");
}
else{
printf("addr_inet.sin_addr:%x\n", addr_inet.sin_addr.s_addr);
}
return 0;
}

inet_aton

将网络字节序整数型 IP 地址转换成为字符串形式。

1
2
3
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr addr);
//成功时返回转换的字符串地址值,失败时返回-1

**返回值是 char* 类型,字符串保存到内部空间,如果返回字符串没有保存,下次调用该函数信息上次信息会丢失*

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
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char* argv[]){
struct sockaddr_in addr1, addr2;
char *str_ptr;
char str_arr[20];

addr1.sin_addr.s_addr = htonl(0x1020304);
addr2.sin_addr.s_addr = htonl(0x1010101);

str_ptr = inet_ntoa(addr1.sin_addr);
strcpy(str_arr, str_ptr);
printf("str_ptr:%s\n", str_ptr);
printf("str_arr:%s\n", str_arr);

// str_ptr 指向地址的内容在函数内部修改了
inet_ntoa(addr2.sin_addr);
printf("str_ptr:%s\n", str_ptr);
printf("str_arr:%s\n", str_arr);
return 0;
}

# output
str_ptr:1.2.3.4
str_arr:1.2.3.4
str_ptr:1.1.1.1
str_arr:1.2.3.4

INADDR_ANY

利用常数 INADDR_ANY 分配服务器端的IP地址,采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。

1
2
3
4
5
6
struct sockaddr_in addr;
char* serv_port = "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));