【网络编程】网络编程基础:TCP/UDP 协议
一、什么是网络?
网络是信息传输,接收和共享的虚拟世界,通过把网络上的信息汇聚在一起,将这些资源进行共享。
初衷:知识共享。这里不得不提到Internet 的历史-它其实是“冷战”的产物:
- 1957年10月和11月,前苏联先后有两颗“Sputnik”卫星上天。
- 1958年美国总统艾森豪威尔向美国国会提出建立DARPA (Defense Advanced Research Project Agency),即国防部高级研究计划署,简称ARPA。
- 1968年6月DARPA提出“资源共享计算机网络” (Resource Sharing Computer Networks),目的在于让DARPA的所有电脑互连起来,这个网络就叫做ARPAnet,即“阿帕网”,是 Interne 的最早雏形。
二、计算机中的软件层面,网络是由什么组成的?
1>IP
//定义
IP地址是一种因特网上的主机编址方式,也称为网际协议地址IP,是任意一台主机
在网络中的唯一标识。
//格式
点分十进制:用户编写,给人看的 192.168.30.100
网络二进制:给系统看的 0 1
//分类
IPv4:(主要集中在电脑)
点分十进制:4个字节
网络二进制:32个位(42亿个IP地址)
由网络地址(子网ID)和主机地址(主机ID)构成
IPv6:(主要用在手机WiFi,目前电脑上用得不多)
点分十进制:16个字节
网络二进制:128个位
IPv4地址分类:A/B/C/D/E
分别应用于大型网、中型网、中小型网、组播型、待用型
//A类地址:政府机关或者学校等大型网络
#规则:以0开头,8位的网络地址(子网ID),24位的主机地址(主机ID)
网络二进制:0000 0000 0000 0000 0000 0000 0000 0001 - 0111 1111 1111 1111 1111 1111 1111 1110
点分十进制: 0.0.0.1 - 127.255.255.254
//B类地址:中等规模的企业使用
#规则:以10开头,16位的网络地址(子网ID),16位的主机地址(主机ID)
网络二进制:1000 0000 0000 0000 0000 0000 0000 0001 - 1011 1111 1111 1111 1111 1111 1111 1110
点分十进制: 128.0.0.1 - 191.255.255.254
//C类地址:任意个人使用 *
#规则:以110开头,24位的网络地址(子网ID),8位的主机地址(主机ID)
网络二进制:1100 0000 0000 0000 0000 0000 0000 0001 - 1101 1111 1111 1111 1111 1111 1111 1110
点分十进制: 192.0.0.1 - 223.255.255.254
//D类地址(组播地址):4个字节都是网络地址 *
#规则:以1110开头,32位的网络地址(子网ID),0位的主机地址(主机ID)
网络二进制:1110 0000 0000 0000 0000 0000 0000 0001 - 1110 1111 1111 1111 1111 1111 1111 1110
点分十进制: 224.0.0.1 - 239.255.255.254
//E类地址:未使用的地址 -- 测试地址
#规则:以11110开头,留着待用
前三位:判断类别
A: 000
B: 100
C: 110
D: 111
注意:有两个地址不能使用
/**
主机地址(主机ID)全为0,它是网段号、网络号
主机地址(主机ID)全为1,它是广播地址
**/
端口号:标识计算机上的进程
取值范围: 0~2^16 -1
//特点
网络地址不同的网络不能直接通信,如果要通信须通过路由器进行转发
2>子网掩码
//定义
子网掩码又叫网络掩码、地址掩码,是一个32位由1和0组成的数值,并且1和0都是连续
//作用
指明IP地址中哪些位表示为子网ID,哪些位表示为主机ID
//特点
必须结合IP地址一起使用,不能单独存在
IP地址中由子网掩码中1覆盖的连续位为子网ID,其余为主机ID
//以C类地址为例
192.168.30.100/255.255.255.0
192.168.30.100/24
前缀长度:24
3>网关
用来管理当前网段下的信息传输、网络的门户,默认取值为1,192.168.30.1,随机取值(1 - 254)
4>DNS域名解析器
DNS的作用类似于电话簿,将域名和IP地址相对应,使得用户可以通过域名来访问网站,
不要记忆复杂的IP地址
www.baidu.com - 183.2.172.42
三、网络体系结构
网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务。网络体系结构即指 网络的层次结构和每层所使用协议的集合。
两类非常重要的体系结构:OSI 与 TCP/IP
OSI开放系统互联模型
OSI模型相关的协议已经很少使用,但模型本身非常通用;OSI模型是一个理想化的模型,尚未有完整的实现,OSI模型共有七层:
1>OSI七层模型
应用层 应用程序:FTP、E-mail、Telnet
----------------------
表示层 数据格式定义、数据转换/加密
----------------------
会话层 建立通信进程的逻辑名字与物理名字之间的联系
----------------------
传输层 差错处理/恢复,流量控制,提供可靠的数据传输
----------------------
网络层 数据分组、路由选择
----------------------
链路层 数据组成可发送、接收的帧
----------------------
物理层 传输物理信号、接口、信号形式、速率
目的:将数据封装,形成一个约定好的通信协议
协议:双方约定好的通信规则
缺点:太复杂,太过理想化,有些层的功能重复
双方通信需要保证协议一致
2>TCP/IP协议族的体系结构
重点学习的体系结构,网络协议中的世界语、标准语
应用层 应用程序:FTP、E-mail、Telnet -- http(超文本传输协议) DNS POP3(邮件接收协议) STMP(邮件发送协议)
----------------------
传输层 TCP/UDP -- 确定数据包交给主机上的那个进程
----------------------
网络层 IP/ICMP/IGMP
----------------------
网络接口和物理层 网卡驱动和物理接口
/**
传输层:TCP/UDP
TCP:微信视频电话 --- 保证对方接了之后才能通信 -- 文件传输、聊天
UDP:微信发消息 --- 只管发,不管对方有无接收 -- 视屏传输
网络层:
IP:主机的唯一标识
ICMP:网络控制消息协议,用于ping命令的实现
IGMP:网络组管理协议,用于广播、组播的实现
**/
/**
网络接口和物理层:
网卡:让不同的计算机之间连接,从而实现数据的通信等功能(有线网卡和无线网卡)
mac(物理地址):网卡的标识号(48位),类似于身份证号,理论上全球唯一
物理地址: 00:0c:29:14:84:69
前三组称为厂商ID,后三组为设备ID
ARP 将IP地址转换为MAC地址
RARP 将MAC地址转换为IP地址
**/
3>数据的封装与传递过程
见 数据的封装与传递过程(如下图所示)
封装的目的:保证数据稳定可靠地传输
发送端 数据打包
接收端 数据解包
数据传输过程:
层层封装 ---> 层层解封
["疯狂星期四Vme50"] ---> 应用层
[TCP/UDP 头]["疯狂星期四Vme50"] ---> 传输层
[IP 头][TCP/UDP 头]["疯狂星期四Vme50"] ---> 网络层
[ARP 头][IP 头][TCP/UDP 头]["疯狂星期四Vme50"] ---> 物理接口层
|
将打包好的数据发送出去
数据在网络中都是以帧的形式发送 [ARP 头][IP 头][TCP/UDP 头]["疯狂星期四Vme50"] -- 1帧数据
[IP 头][TCP/UDP 头]["疯狂星期四Vme50"] -- 最大 1500个 字节
TCP/IP 协议下的数据包:
4>端口号
类似于PID表示一个进程,区分不同的网络应用程序
例如:
QQ 4399 微信 17173
无符号短整型 unsigned short 2个字节 0 ~ 65535
1 ~ 1023 系统进程使用
1024 ~ 5000 系统分配的端口
5001 ~ 65535 用户自己分配使用的
问:为什么有进程PID了,还要搞一个端口号来区分不同的应用程序?
答:根据进程运行被分配的进程号不固定,但想以固定的序号去访问指定的应用程序,便有了端口号
5>大小端序
不同类型CPU主机中,内存存储的整数字节序分为以下两种:
小端序(little-endian)
低位字节存储在低位地址 linux,x86平台
大端序(big-endian)
低位字节存储在高位地址 网络字节序
//用于大小端序转换的工具函数
#include <arpa/inet.h>
//主机字节序转换为网络字节序(小端序转换为大端序)
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
//网络字节序转换为主机字节序(大端序转换为小端序)
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
6>网络通信模型:
C/S : 服务器 + 客户端
B/S : 网页端 http https
四、TCP/UDP的特点和区别
都采用经典的C/S模型,即客户端(client)服务器(server)模型。
不同点:TCP:有连接,可靠;UDP:无连接,不保证可靠。
1》TCP: 面向链接的可靠协议 -- 1对1私聊
//定义
是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)
借助于C/S模型搭建TCP通信:
服务器(while(1)) 客户端
1>建立socket连接 -- 买手机 1> 建立socket连接 -- 买手机
2>绑定IP和端口号 -- 办电话卡 2> 连接服务器
3>监听 -- 开机 3> 收/发
4>等待连接 -- 等别人打电话
5>收/发
//功能
提供不同主机上的进程通信
//特点
1.建立连接->使用连接->释放连接
2.TCP数据包中包含序号和确认序号
3.对包进行排序并排错,而损坏的包可以被重传
//适用情况:
适合于对传输质量要求较高,以及传输大量数据的通信。
在需要可靠数据传输的场合,通常使用TCP协议s
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
/**********************************************************************
TCP的建立连接和断开连接
标识符:
ACK:确认标识符
FIN:断开标识符
SYN:请求标识符 -- 请求服务器连接
URG:紧急标识符
PSH:推标识
TCP的三次握手: (如何打通电话)
1.客户端给服务器发送SYN连接请求
2.服务器回复ACK,并给客户端发送连接请求
3.客户端回复ACK
TCP的四次挥手: (如何挂断电话)
1.客户端给服务器发送FIN请求
2.服务器回复ACK
3.服务器给客户端发送FIN请求
4.客户端回复请求ACK
**********************************************************************/
2》UDP
//定义
是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
//功能
提供不同主机上的进程通信
//特点
1.发送数据之前不需要建立连接
2.不对数据包的顺序进行检查
3.没有错误检测和重传机制
//适用情况
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)
适合于广播/组播式通信中。
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
TCP传输
五、基于TCP协议的网络通信模型
1》框架
/**框架**/
服务器(server) 客户端(client)
1.创建流式套接字:socket();
|
2.绑定本地地址:bind();
|
3.设置监听套接字:listen(); 1.创建流式套接字:socket();
| |
4.等待客户端连接:accept(); 2.发送连接请求:connect();
| |
5.开始和客户端通信:read()/recv(); 3.与服务器通信:write()/send();
| |
6.断开连接:close(); 4.发送断开请求:close();
2》TCP编程相关的API函数
1>socket创建套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/**********************************************************************
@brief: 创建一个特殊的文件描述符(具有网络属性)
@domain: 地址族
AF_UNIX, AF_LOCAL 用于本地进程间通信,域通信
AF_INET IPv4 Internet protocols
AF_INET6 IPv6 Internet protocols
@type: SOCK_STREAM // 流式套接字
SOCK_DGRAM // 数据报套接字
SOCK_RAW // 原始套接字
@protocol: 一般默认设置为0,表示前面两个参数有效
@retval: 成功:返回具有网络属性的文件描述符
失败:返回-1,并且设置全局错误码
**********************************************************************/
2>connect主动连接服务器(绑定IP和端口号)
bind()
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/**********************************************************************
@brief: 主动连接服务器
@sockfd: 客户端的唯一文件描述符
@addr: 声明服务器的结构体指针
@addrlen: 结构体的长度
@retval: 成功:返回0
失败:返回-1,并且设置全局错误码
**********************************************************************/
/**********************************************************************************
grep查询struct sockaddr{}的具体实现
grep struct\ sockaddr\ { /usr/src/linux-headers-5.4.0-150-generic/include/* -rn
vim /usr/src/linux-headers-5.4.0-150-generic/include/linux/socket.h +31
=================================================================================
ctags查询struct sockaddr{}的具体实现
1》安装ctags
sudo apt-get install ctags
2》去到头文件所在的目录
cd /usr/src/linux-headers-5.4.0-150-generic/include/
3》生产ctags标签
sudo ctags -R
4》查询
vim -t 类(例如:sockaddr)
=================================================================================
locate 文件名 用于查询该文件所在路径
**********************************************************************/
#通用地址结构
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */ //地址族 --> ip地址
char sa_data[14]; /* 14 bytes of protocol address */ //IP + 端口号
};
struct sockaddr
{
u_short sa_family; // 地址族, AF_xxx
char sa_data[14]; // 14字节协议地址
};
// 该结构体不适用: 1>IP + Prot = 6 个字节,sa_data有14个字节,多的字节没法填充
// 2>无法判断IP在前面还是Prot在前面
/*
问题:
1.IP地址和端口号谁在前谁在后?
2.IP地址和端口号总共占6个字节,多出来的8个字节怎么处理?
*/
#Internet协议地址结构
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
struct sockaddr_in {
u_short sin_family; // 地址族, AF_INET,2 bytes
u_short sin_port; // 端口,2 bytes
struct in_addr sin_addr; // IPV4地址,4 bytes
char sin_zero[8]; // 8 bytes unused,作为填充
};
// internet address
struct in_addr
{
in_addr_t s_addr; // u32 network address
};
/**点分十进制和网络二进制的相互转换函数**/
in_addr_t inet_addr(const char *cp);
/**********************************************************************
@brief: 点分十进制转换为网络二进制
@cp: 字符串点分十进制
@retval: 成功:返回网络二进制数
失败:返回-1,并且设置全局错误码
**********************************************************************/
char *inet_ntoa(struct in_addr in);
/**********************************************************************
@brief: 网络二进制转换为点分十进制
@cp: 网络二进制的结构体变量
@retval: 成功:返回字符串点分十进制
失败:返回-1,并且设置全局错误码
**********************************************************************/
3>send发送
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/**********************************************************************
@brief: 发送数据
@sockfd: 套接字文件描述符
@buf: 发送数据的容器
@len: buf的长度(用strlen)
@flags: 一般为0
@retval: 成功:返回实际发送的字节数
失败:返回-1,并且设置全局错误码
**********************************************************************/
4>close关闭
#include <unistd.h>
int close(int fd);
5>recv接收
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/**********************************************************************
@brief: 接收数据
@sockfd: 套接字文件描述符,注意如果是服务器应使用accept对应的套接字
@buf: 接收数据的容器
@len: buf的长度(用sizeof)
@flags: 一般为0
@retval: 成功:返回实际接收的字节数
0:异常退出
失败:返回-1,并且设置全局错误码
**********************************************************************/
6>bind绑定
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/**********************************************************************
@brief: 将IP地址和端口号与套接字绑定在一起
@sockfd: 套接字文件描述符
@addr: 服务器的地址信息的结构体指针
@addrlen: 结构体的长度
@retval: 成功:返回0
失败:返回-1,并且设置全局错误码
**********************************************************************/
7>listen监听
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/**********************************************************************
@brief: 保护服务器,限制同一时间客户端最大的连接数量
@sockfd: 套接字文件描述符
@backlog: 同一时间最大连接数量
@retval: 成功:返回实际发送的字节数
失败:返回-1,并且设置全局错误码
**********************************************************************/
8>accept等待客户端的连接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/**********************************************************************
@brief: 等待客户端连接(阻塞)
@sockfd: 套接字文件描述符
@addr: 客户端的结构体指针
@addrlen: 结构体的长度的指针
@retval: 成功:返回与客户端进行连接通信的文件描述符
失败:返回-1,并且设置全局错误码
**********************************************************************/
基于 TCP协议 的服务器-客户端通信模型的 C 语言代码示例
该代码实现了基本的网络通信框架,其中服务器监听客户端连接,接受连接后进行数据收发,客户端与服务器建立连接并进行数据交换。
TCP 服务器端代码(server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080 // 服务器监听端口
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建流式套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket failed");
exit(EXIT_FAILURE);
}
// 2. 绑定本地地址
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 3. 设置监听套接字
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 等待客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("Accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Client connected!\n");
// 5. 开始和客户端通信
while (1) {
memset(buffer, 0, BUFFER_SIZE);
int bytes_read = read(new_socket, buffer, BUFFER_SIZE);
if (bytes_read <= 0) {
printf("Client disconnected.\n");
break;
}
printf("Received: %s\n", buffer);
// 发送回复
char *message = "Message received.";
send(new_socket, message, strlen(message), 0);
}
// 6. 断开连接
close(new_socket);
close(server_fd);
return 0;
}
TCP 客户端代码(client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1" // 服务器地址
#define PORT 8080 // 服务器端口
#define BUFFER_SIZE 1024
int main() {
int sock;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
// 1. 创建流式套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation error");
exit(EXIT_FAILURE);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 2. 设置服务器地址
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("Invalid address / Address not supported");
close(sock);
exit(EXIT_FAILURE);
}
// 3. 发送连接请求
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection failed");
close(sock);
exit(EXIT_FAILURE);
}
printf("Connected to server.\n");
// 4. 与服务器通信
while (1) {
printf("Enter message: ");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "\n")] = 0; // 去掉换行符
send(sock, buffer, strlen(buffer), 0);
memset(buffer, 0, BUFFER_SIZE);
int bytes_read = read(sock, buffer, BUFFER_SIZE);
if (bytes_read <= 0) {
printf("Server disconnected.\n");
break;
}
printf("Server: %s\n", buffer);
}
// 5. 发送断开请求
close(sock);
return 0;
}
运行步骤
- 编译代码
gcc server.c -o server
gcc client.c -o client
- 启动服务器
./server
- 启动客户端
./client
- 在客户端发送消息
客户端输入消息后,服务器会接收到并回复 Message received.
该代码实现了基于 TCP 协议的网络通信模型:
服务器:
socket() 创建套接字
bind() 绑定 IP 和端口
listen() 监听连接
accept() 接受客户端连接
read()/send() 进行数据通信
close() 断开连接
客户端:
socket() 创建套接字
connect() 连接服务器
send()/read() 进行数据通信
close() 断开连接
综上。希望该内容能对你有帮助,感谢!
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!