1 前序知识

1.1 局域网和广域网

局域网:将一定区域内的设备(计算机、外部设备、数据库等)连接起来形成的计算机通信的私有网络

广域网:又称为外网和公网。连接不同地区局域网或城域网的远程公用网络

1.2 IP地址

分为ipv4ipv6地址

1.2.1 IPv4

  • 使用一个32位的整型数表示一个IP地址,4字节,int型
  • 使用点分十进制表示IP地址:192.168.1.1
  • 分成了4份,每份1字节(8bit),最大值为255

因此,一共可用IP地址 232

1.2.2 IPv6

  • 使用128位表示一个IP地址,16字节
  • 使用字符串描述IP:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
  • 分成了8份,每份2字节,每一部分都按16进制表示

共2128个可用IP地址

1.3 端口

端口的作用是定位主机上的一个进程,进程可以通过这个端口进行网络通信

端口是一个短整型无符号数usigned short,16位有效数,范围0~65535

注意:

  • 并不是所有进程都需要绑定端口,如果某个进程不需要网络通信,就无需绑定
  • 一个端口只能给一个进程使用,不能多进程公用

1.4 网络分层模型

ISO/OSI网络模型-哔哩哔哩讲解

image-20240308220727062

数据流通过程:

image-20240308235500469

image-20240309000750086
  • 物理层:数据比特流的传输
  • 数据链路层:负责将数据分帧和封装成帧,并进行差错控制和流量控制,提供介质访问和链路管理。
  • 网络层:IP选址及路由选择。
  • 传输层:建立端到端的连接
  • 会话层:建立、管理和维护表示层实体的会话。
  • 表示层:数据格式转化,对数据进行加密。
  • 应用层:为应用程序提供服务。

1.5 三次握手四次挥手

TCP协议是一个可靠的面向连接的流式传输协议,所谓的面向连接就是三次握手

img
img
  • SYN:连接请求/接收 报文段
  • seq:发送的第一个字节的序号
  • ACK:确认报文段
  • ack:确认号。希望收到的下一个数据的第一个字节的序号

1.5.1 为什么是三次握手?

三次握手的作用:确认双方连接正常。

  1. 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
  2. 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
  3. 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

如果不采用三次握手,客户端发送连接请求,但是网络产生了延迟,此时客户端可能会认为连接未能建立成功,又重新发起了新的连接请求。服务器接收到客户端发来的两个连接请求都给予了回应确认,就会建立两个相互独立的连接。导致资源浪费和数据混乱。

1.5.2 为什么是四次挥手?

四次挥手的作用:确保双方都能够安全地关闭TCP连接

1、客户端执行主动关闭,发送 FIN的包(FIN),表示客户端的数据发送完毕。

2、服务端执行被动关闭,发送确认 ACK 包。

3、服务端给客户端发送 FIN,告诉客户端我也要关闭。

4、客户端确认服务端的ACK的包,确认服务端关闭。

1.5.3 2MSL的作用?

MSL是报文的最大生存时间(单向数据传输最大有效时间,根据跳数设定值)

TIME_WAIT状态也就是2MSL等待状态。

客户端发出的第四次挥手ACK报文,最大有效时间为MSL,也就是说必须在MSL的时间内达到服务端。超过这个时间服务端就会重传第三次挥手的FIN报文,相同的必须在MSL时间内到达。

若出现ACK报文丢失,客户端在发出第四次挥手ACK报文和接收到重传的第三次挥手FIN报文的时间间隔就为2MSL。

所以等待2MSL是确保连接双方关闭完成。

2 网络编程

img

2.1 创建socket

1
int socket(int domain, int type, int protocal);
  • domain 通信的协议家族
    • PF_INET:ipv4
    • PF_INET6:ipv6
    • PF_LOCAL:本地通信的协议族
    • PF_PACKET:内核底层协议族
    • PF_IPX:IPX Novell协议族
  • type 数据传输的类型
  • SOCK_STREAM:面向连接的socket
  • SOCK_DGRAM:无连接的socket
  • protocal 最终使用的协议
    • IPPROTO_TCP
    • IPPROTO_UDP
    • 可以填0,编译器会自动识别类型
1
2
3
4
5
socket(PF_INET, SOCK_STREAM, 0);
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)//等效

socket(PF_INET, SOCK_DEGRAM, 0);
socket(PF_INET, SOCK_DEGRAM, IPPROTO_UDP);

返回值:

  • 成功:返回对应的sockfd文件描述符
  • 失败:返回-1

2.2 主机字节序和网络字节序

大端序:低位字节存放在高位,高位字节存放在低位

1
2
3
4
0x001	0x12
0x002 0x34
0x003 0x56
存放123456

小端序:低位字节存放在低位,高位字节存放在高位

1
2
3
0x001	0x56
0x002 0x34
0x003 0x12

小端序更便于CPU进行处理,大端序便于人类查看

2.2.0 相关函数

不同的主机可能是采用不同的字节序,因此出现了网络字节序,本质上是大端序

2.2.0.1 htons/htonl

1
2
3
4
5
6
#include <arpa/inet.h> 
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);

uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
  • h:host 主机
  • to:转换
  • n:network 网络
  • s:short(2字节,16位整数)
  • l:long(4字节,32位整数)

长整型用于转换IP地址,短整型用于转换端口号,但平时IP地址都是用点分十进制形式表示,因此,需要使用其他函数将点分十进制转换为整数型

2.2.0.2 inet_pton 和 inet_ntop

p:people read人类可读

1
int inet_pton(int af, const char *src, void *dst);
  • af:协议族,AF_INET或AF_INET6
  • src:点分十进制的地址
  • dst:接收转换后的数据

返回值:

  • 成功:1
  • 失败:负数
  • 格式不对:0
1
char *inet_ntop(int af, const void *src, char *dst, size_t len);
  • af:AF_INET或AF_INET6
  • src:指向网络字节序的指针
  • dst:指向转换后的字符串指针
  • len:目标大小,防止溢出

返回值:

  • 成功:结果指针
  • 出错:NULL

IPv4地址采用4字节的整数存放,端口号用2字节的整数存放(0~65536)

2.2.1 结构体

2.2.1.1 sockaddr 结构体

存放协议族、端口和地址信息,客户端connect()和服务端bind()需要此结构体

1
2
3
4
5
6
7
struct sockaddr{
unsigned short sa_family;
//协议族,AF_INET,和socket()的第一个参数相同,
//AF和PF数值上没区别,可以混用
unsigned char sa_data[14];
//14字节的端口和地址
}

2.2.1.2 sockaddr_in 结构体

sockaddr结构体操作不方便,所以出现了sockaddr_in

两者大小相等,因此可以进行强制类型转换

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in{
unsigned short sin_family;//协议族
unsigned short sin_port;//16位端口号
struct in_addr sin_addr; //32位的地址
unsigned char sin_zero[8]; //未使用,为了保持与sockaddr长度一样而设置
}

struct in_addr{
unsigned int s_addr;//32位的地址
}

2.3 bind函数

1
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen);
  • sockfd:服务端监听文件描述符
  • addr:包含ip地址和端口号的sockaddr结构体
  • addrlen:第二个参数(结构体)的长度(sizeof)

2.4 listen函数

1
int listen(int sockfd, int backlog);
  • sockfd:服务端文件描述符
  • backlog:最大连接数目

2.5 connect函数

1
int connect(int sockfd, struct sockaddr* addr, socklen_t addrlen);
  • sockfd:客户端通信文件描述符
  • addr:包含ip地址和端口号的sockaddr结构体
  • addrlen:第二个参数(结构体)的长度(sizeof)

2.6 accept函数

1
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
  • sockfd:服务端监听文件描述符
  • addr:传出参数,记录连接成功的客户端信息地址和端口号
  • addrlen:第二个参数的长度

返回值:

  • 成功:服务端用于通信的文件描述符
  • 失败:-1

2.7 read、write函数

1
2
ssize_t write(int fd, const void *buf, size_t count);      // 写数据 
ssize_t read(int fd, void *buf, size_t count); // 读数据

网络I/O操作不止是read和write

1
2
3
4
5
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

声明如下

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

2.8 close函数

1
int close(int fd);

2.9 实例

2.9.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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>

#define SERVER_PORT 8888

int main()
{

//客户端只需要一个套接字文件描述符,用于和服务器通信
int serverSocket;

//描述服务器的socket
struct sockaddr_in serverAddr;

char sendbuf[200]; //存储 发送的信息
char recvbuf[200]; //存储 接收到的信息

int iDataNum;


/*********************************************************************/
/* 1-创建客户端套接字 */
/*********************************************************************/
if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}

serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVER_PORT);

//指定服务器端的ip,本地测试:127.0.0.1
//inet_addr()函数,将点分十进制IP转换成网络字节序IP
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

/*********************************************************************/
/* 2-连接服务端 */
/*********************************************************************/
if(connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
{
perror("connect");
return 1;
}

printf("连接到主机...\n");

/*********************************************************************/
/* 3-发送接收消息 */
/*********************************************************************/
while(1)
{
printf("发送消息:");
scanf("%s", sendbuf);
printf("\n");
write(serverSocket, sendbuf, strlen(sendbuf)); //向服务端发送消息
if(strcmp(sendbuf, "quit") == 0) break;
printf("读取消息:");
recvbuf[0] = '\0';
iDataNum = read(serverSocket, recvbuf, 200); //接收服务端发来的消息
recvbuf[iDataNum] = '\0';
printf("%s\n", recvbuf);
}

close(serverSocket);

return 0;
}

2.9.2 服务端

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>

#define SERVER_PORT 8888

/*
监听后,一直处于accept阻塞状态,
直到有客户端连接,
当客户端如close后,断开与客户端的连接
*/

int main()
{

//调用socket函数返回的文件描述符
int serverSocket;

//声明两个套接字sockaddr_in结构体变量,分别表示客户端和服务器
struct sockaddr_in server_addr;
struct sockaddr_in clientAddr;

int addr_len = sizeof(clientAddr);
int clientSocket;
char buffer[200]; //存储 发送和接收的信息
int iDataNum;


/*********************************************************************/
/* 1-创建服务端套接字 */
/*********************************************************************/
if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}
memset(&server_addr,0, sizeof(server_addr));

//初始化服务器端的套接字,并用htons和htonl将端口和地址转成网络字节序
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);

//ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);



//对于bind,accept之类的函数,里面套接字参数都是需要强制转换成(struct sockaddr *)
//bind三个参数:服务器端的套接字的文件描述符
/*********************************************************************/
/* 2-服务端绑定监听的IP和por */
/*********************************************************************/
if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("connect");
return 1;
}


/*********************************************************************/
/* 3-服务端开始监听 */
/*********************************************************************/
if(listen(serverSocket, 5) < 0)//开启监听 ,第二个参数是最大监听数
{
perror("listen");
return 1;
}

/*********************************************************************/
/* 4-接收发送消息 */
/*********************************************************************/
printf("监听端口: %d\n", SERVER_PORT);

//调用accept函数后,会进入阻塞状态
//accept返回一个套接字的文件描述符,这样服务器端便有两个套接字的文件描述符,
//serverSocket和client。
//serverSocket仍然继续在监听状态,client则负责接收和发送数据
//clientAddr是一个传出参数,accept返回时,传出客户端的地址和端口号
//addr_len是一个传入-传出参数,传入的是调用者提供的缓冲区的clientAddr的长度,以避免缓冲区溢出。
//传出的是客户端地址结构体的实际长度。
//出错返回-1

clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, (socklen_t*)&addr_len);

if(clientSocket < 0)
{
perror("accept");

}

printf("等待消息...\n");

//inet_ntoa ip地址转换函数,将网络字节序IP转换为点分十进制IP
//表达式:char *inet_ntoa (struct in_addr);
printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr)); //把来访问的客户端的IP地址打出来
printf("Port is %d\n", htons(clientAddr.sin_port));

while(1)
{
buffer[0] = '\0';
iDataNum = read(clientSocket, buffer, 1024);
if(iDataNum < 0)
{
continue;
}
buffer[iDataNum] = '\0';
if(strcmp(buffer, "quit") == 0) break;
printf("收到消息: %s\n", buffer);
printf("发送消息:");
scanf("%s", buffer);
write(clientSocket, buffer, strlen(buffer)); //服务端也向客户端发送消息
if(strcmp(buffer, "quit") == 0) break; //输入quit停止服务端程序
}

close(clientSocket);
close(serverSocket);

return 0;

}