1 前序知识
1.1 局域网和广域网
局域网:将一定区域内的设备(计算机、外部设备、数据库等)连接起来形成的计算机通信的私有网络
广域网:又称为外网和公网。连接不同地区局域网或城域网的远程公用网络
1.2 IP地址
分为ipv4和ipv6地址
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 为什么是三次握手?
三次握手的作用:确认双方连接正常。
- 第一次握手:Client 什么都不能确认;Server
确认了对方发送正常,自己接收正常
- 第二次握手:Client
确认了:自己发送、接收正常,对方发送、接收正常;Server
确认了:对方发送正常,自己接收正常
- 第三次握手: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
| char *inet_ntop(int af, const void *src, char *dst, size_t len);
|
- af:AF_INET或AF_INET6
- src:指向网络字节序的指针
- dst:指向转换后的字符串指针
- len:目标大小,防止溢出
返回值:
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; unsigned char sa_data[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; struct in_addr sin_addr; unsigned char sin_zero[8]; }
struct in_addr{ unsigned int s_addr; }
|
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:第二个参数的长度
返回值:
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函数
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; struct sockaddr_in serverAddr; char sendbuf[200]; char recvbuf[200]; int iDataNum; 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); serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) { perror("connect"); return 1; } printf("连接到主机...\n"); 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
int main() { int serverSocket; struct sockaddr_in server_addr; struct sockaddr_in clientAddr; int addr_len = sizeof(clientAddr); int clientSocket; char buffer[200]; int iDataNum; if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0) { perror("socket"); return 1; } memset(&server_addr,0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("connect"); return 1; } if(listen(serverSocket, 5) < 0) { perror("listen"); return 1; } printf("监听端口: %d\n", SERVER_PORT); clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, (socklen_t*)&addr_len); if(clientSocket < 0) { perror("accept"); } printf("等待消息...\n"); printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr)); 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; } close(clientSocket); close(serverSocket); return 0; }
|