一,TCP/IP
计算机⽹络就是把各个计算机连接到⼀起,让⽹络中的计算机可以互相通信,网络编程解决的是如何在程序中实现两台计算机的通信。
为了把全世界的所有不同类型的计算机都连接起来,就必须规定⼀套全球通⽤的协议,为了实现互联网这个⽬标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。有了Internet,任何私有网络,只要支持这个协议,就可以联入互联网。
互联⽹协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所
以,⼤家把互联⽹的协议简称TCP/IP协议。
1.IP协议简介
1)IP协议
IP协议负责把数据从⼀台计算机通过⽹络发送到另⼀台计算机。数据被分割成⼀⼩块⼀⼩块,然后通过IP包发送出去,由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把⼀个IP包转发出去。IP协议处于网络层。
IP (internet Protocol) ⽹际协议位于⽹络层。⼏乎所有使⽤⽹络的系统都会⽤到IP 协议。
IP 协议的作⽤是把各种数据包传送给对⽅。保证送达的前置条件是:
(1)IP 地址:指明节点被分配到的地址
(2)MAC 地址:指⽹卡所属的固定地址
(3)数据包在这些节点之间的移动都是由ARP(Address Resolution Protocol:地址解析协议)负责将IP地址映射到MAC地址上来完成的。
区别:
IP 地址的设计是出于拓扑设计出来的,只要在不重复 IP 地址的情况下,它是可以随意更改的;⽽ MAC 地址是根据⽣产⼚商烧录好的,它⼀般不能改动的,IP 地址更像是精确到⻔牌号的地址信息,⽽ MAC 地址则是拥有唯⼀身份证号的“⼈”。
2)IP地址
IP协议中还有⼀个⾮常重要的内容,那就是给因特⽹上的每台计算机和其它设备都规定了⼀种地址,叫做“IP 地址”,IP地址的作⽤是标识计算机的⽹卡地址,每台计算机都有⼀个IP地址,在程序中是通过IP地址来访问⼀台计算机的。
在window上查看⽹络信息在设置网络查看,在linux下可以使⽤ifconfig查看网络信息
IP地址分类:IPv4地址和IPv6地址两种。
IPV4地址由两部分组成,即⽹络地址和主机地址。
网络地址表示其属于互联⽹的哪⼀个⽹络,主机地址表示其属于该⽹络中的哪⼀台主机。⼆者是主从关系。 根据⽹络号和主机号的不同,IP地址⼜可分为A,B,C,D,E类。其中A,B,C类地址经常使用,⽽D和E类是特殊地址,不经常使用。

A类:(1.0.0.0-126.0.0.0)
B类:(128.0.0.0-191.255.0.0)
C类:(192.0.0.0-223.255.255.0)
D类:是多播地址。该类IP地址的最前⾯为“1110”,所以地址的⽹络号取值于224~239之间。⼀般⽤于多路⼴播⽤户。
E类:是保留地址。该类IP地址的最前⾯为“1111”,所以地址的⽹络号取值于240~255之间。
IP地址⼜分为公⽹IP地址和内⽹IP地址。
公⽹IP地址可以直接被访问,内⽹IP地址只能在内⽹访问。要注意的是公⽹ip具有世界范围的唯⼀性,⽽内⽹ip只在局域⽹内部具有唯⼀性。并且,⼀个局域⽹⾥所有电脑的内⽹IP是互不相同的,但共⽤⼀个外⽹IP。
在IP地址3种主要类型⾥,各保留了3个区域作为私有地址(⽐如内部局域⽹所⽤的
地址),其地址范围如下:
A类地址:10.0.0.0~10.255.255.255
B类地址:172.16.0.0~172.31.255.255
C类地址:192.168.0.0~192.168.255.255
其中特殊的IP:
127.0.0.1
有⼀个特殊的IP地址,127.0.0.1,称之为本机地址,它总是127.0.0.1。⼀般⽤于测试使⽤或本地进程间通信⽤,其对应的本机域名为localhost。
0.0.0.0
在IPV4中,0.0.0.0地址被⽤于表示⼀个⽆效的,未知的或者不可⽤的⽬标。但是
在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果⼀个主机有两个IP地
址,192.168.1.1 和 10.1.2.1,并且该主机上的⼀个服务监听的地址是0.0.0.0,那么通
过两个ip地址都能够访问该服务。
3)域名
域名是⽤来代替ip地址来表示计算机的⼀种直观的名称。⽐如:www.baidu.com就是⼀个域名。由于直接记忆IP地址⾮常困难,所以我们通常使⽤域名访问某个特定的服务。域名解析服务器DNS负责把域名翻译成对应的IP,客户端再根据IP地址访问服务器。
可以使⽤ping命令查看⼀个域名对应的IP地址。

4)端口
在两台计算机通信时,只发IP地址是不够的,因为同⼀台计算机上跑着多个⽹络程序。⼀个IP包来了之后,到底是交给浏览器还是QQ,就需要端⼝号来区分。每个⽹络程序都向操作系统申请唯⼀的端⼝号,这样,两个进程在两台计算机之间建⽴⽹络连接就需要各⾃的IP地址和各⾃的端⼝号。 端⼝号是⼀个16位的⽆符号整数,对应的⼗进制取值范围为0~65535。端⼝号不是随意使⽤的,⽽是按照⼀定的规定进⾏分配。低于256号的端⼝是系统的保留端⼝,主要⽤于系统进程通信。⽐如⽹站的www服务使⽤的是80端⼝,FTP服务使⽤的21号端⼝。我们开发中使⽤的端⼝建议使用1024以后的动态端⼝。
端⼝查看:
在windows下查看所有的端⼝占⽤信息可以使⽤: netstat -ano
在linux下可以使⽤: ss -lntpd 或⽤netstat查看。
按对应的协议类型,端⼝有两种:TCP端⼝和UDP端⼝。由于TCP和UDP 两个协议是独⽴的,因此各⾃的端⼝号也相互独⽴,⽐如TCP有235端⼝,UDP也 可以有235端⼝,两者并不冲突。
2.TCP协议
TCP协议则是建⽴在IP协议之上的。TCP协议负责在两台计算机之间建⽴可靠连接,保证数据包按顺序到达。TCP协议会通过握⼿建⽴连接,然后,对每个IP包编号,确保对⽅按顺序收到,如果包丢掉了,就⾃动重发。
许多常⽤的更⾼级的协议都是建⽴在TCP协议基础上的,⽐如⽤于浏览器的HTTP协议、发送邮件的SMTP协议等。
1)TCP的数据包
以太⽹数据包(packet)的⼤⼩是固定的,最初是1518字节,后来增加到1522字节。其中, 1500 字节是负载(payload),22字节是头信息(head)。IP头和TCP头都⾄少需要20个字节,因此发送的数据⼤概在1400个字节左右。

TCP 数据包在 IP 数据包的负载⾥⾯,⽤户数据进⼊TCP/IP协议栈的封装过程 如下:
TCP数据包格式:
源端⼝和⽬的端⼝:
各为16⽐特,⽤于表示应⽤层的连接。源端⼝表示产⽣数据包的应⽤层进程,而目的端⼝则表示数据包所要到达的⽬的进程。
32位序列号:
占32⽐特。⽤来标识从TCP源端向TCP⽬标端发送的数据字节流,它表示在这个报⽂段中的第⼀个数据字节。
确认号字段:
占32⽐特。只有ACK标志为1时,确认号字段才有效。它包含⽬标端所期望收到源端的下⼀个数据字节。
标志位字段(U、A、P、R、S、F):占6⽐特。各比特的含义如下:
◆URG:紧急指针(urgent pointer)有效。
◆ACK:确认序号有效。
◆PSH:接收⽅应该尽快将这个报⽂段交给应⽤层。
◆RST:重建连接。
◆SYN:发起⼀个连接。
◆FIN:释放⼀个连接。
16位窗⼝:
16位,表示源端主机在请求接收端等待确认之前需要接收的字节数。它⽤于流量控制,窗⼝⼤⼩根据⽹络拥塞情况和资源可⽤性进⾏增减。 对⽅不运⾏发送超过窗⼝约定⼤⼩的数据,防⽌接收区满了造成溢出。
选项字段:占32比特。可能包括"窗⼝扩⼤因⼦"、"时间戳"等选项。
数据:实际要发送的数据。
2)TCP协议的三次握⼿
在TCP/IP协议中,TCP协议提供可靠的连接服务,采⽤三次握⼿建⽴⼀个连接,所谓面向连接,就是计算机双⽅通信时必须先建⽴连接,然后进⾏数据通信。最后断开连接。

32位确认号(ack):其数值等于发送⽅的发送序号 +1(即接收⽅期望接收的下⼀个序列号)。
TCP在建⽴连接时,有以下三个步骤:
第⼀次握⼿)
客户端发送⼀个 SYN包 (包含有同步序列编号,是32位整数,该数值的分配是根据时间产⽣的⼀个随机值)。包含有同步序列号的标志位的数据段和通信请求给服务器,客户端进⼊SYN_SENT 状态,然后等待服务器的回发确认信息;
第⼆次握⼿)
服务器发⼀个 SYN+ACK 给客户端,确认已经收到客户端发来的信息,此时服务器进⼊SYN_RECV状态;
第三次握⼿)
客户端接收到服务器发来的确认信息后,再反馈⼀个 ACK给服务器,完成三次握⼿,客户端和服务器进⼊ESTABLISHED状态,到此⼀个TCP连接就完成了。
3) 四次挥⼿过程
所谓四次挥⼿(Four-Way Wavehand )即终⽌TCP连接,就是指断开⼀个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。

第⼀次断开:客户端向服务器端FIN报⽂(将 FIN置1),告诉服务器客户端没有要发送的数据了。
第⼆次断开:服务器接收到FIN后, 返回⼀个ack(收到序号+1), 告知主动⽅“我知道你想断开连接的请求了”
第三次断开:处理完客户端的报⽂后,服务器发送给客户端FIN包⽂,发送完FIN报⽂后,服务器进⼊LAST_ACK阶段(超时等待)。客户端收到后关闭客户端的读通道。
LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认。
第四次断开:客户端发回ACK报⽂确认,并将确认序号设置为收到序号加1,此服务器收到后关闭服务器的写通道。
注意:
服务器和客户机都可以发起关闭,完全对称
当⼀个连接被建⽴或被终⽌时,交换的报⽂段只包含TCP头部,⽽没有数据。
3.网络模型
⽹络采⽤分⽽治之的⽅法设计,将⽹络的功能划分为不同的模块,以分层的形式有机结合在⼀起。 每层实现不同的功能,其内部实现的⽅法对外部其他层次来说是透明的,每层向上⼀层提供服务,使⽤下⼀层提供的服务; ⽹络体系结构即⽹络的层次结构和每层使⽤的协议的集合 。
两类⾮常重要的体系结构:
开放式系统互联(open system interconnection 简称 OSI) 和TCP/IP 。
1)OSI⽹络模型
OSI 系统模型是国际化标准组织(ISO)为了实现计算机⽹络标准化颁布的参考模型,根据⽹络中数据传输的过程,将该模式分为7个层,每⼀层都向上⼀层提供服务,同时使⽤下层提供的服务。
会话层的主要功能是负责维护两个节点之间的传输联接,确保点到点传输不中断,以及管理数据交换等功能。
表示层负责数据格式的转化、加密解密等操作数据链路层是以帧
物理层就讲双绞线上的的差模信号转换成0和1,也叫数模转换。
2)TCP/IP系统模型
国际化标准组织(ISO)指定的 OSI 参考模型虽然规定得⼗分细致和完善,但在实际中却得不到⼴泛应⽤,原因就是它过于庞⼤和复杂,但它仍是此后众多协议模型的基础,与OSI 的复杂相⽐,TCP/IP 协议的四层结构模型获得了更⼴泛的使⽤。TCP/IP 协议是Internet 事实上的⼯业标准。
1. 应⽤层:处理⽤于程序的逻辑
2. 传输层:为两台主机上的应⽤程序实现端到端的通信
3. ⽹络层:实现数据包的选路和转发。在应⽤层看来,两台主机是直连的,但实际上并不是,传输中会有很多节点,⽹络层来做传输的节点选择。
4. 数据链路层 :实现⽹卡接⼝驱动,处理⼆进制数据在物理媒介上的传输
3) 两种模型对比
TCP/IP模型的网络接口层相当于OSI模型的物理层和数据链路层,应用层相当于会话层、表示层、应用层。
二,UDP
1.UDP协议特点
UDP(⽤户数据报协议),是⼀种⾯向⽆连接的不可靠的传输协议,位于传输层,不需要通过3次握⼿来建⽴⼀个连接,因此它具有以下的特点。
1.无连接,不可靠;
2. 由于传输数据不建⽴连接,因此也就不需要维护连接状态,包括收发状态等,因此⼀台服务机可同时向多个客户机传输相同的消息;
3. UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小
4 .进程发送的报⽂较短,同时对报⽂的可靠性要求不⾼,那么可以使⽤UDP协议。
UDP 数据包格式

源端⼝和⽬的端⼝:表示发送⽅和接收⽅的端⼝号。
UDP包⻓度: 包括UDP⾸部在内的以字节为单位的UDP数据报总⻓度;
校验和: 【校验算法】
2.TCP协议与UDP协议
(1)TCP协议与UDP协议
TCP协议与UDP协议是⼀个负责数据封装与打包的协议,它们都是传输层协议。
(2)两者的区别
·TCP是面向连接的,即使⽤TCP协议传输数据需要先建⽴连接,如打电话需要先拨号接通电话;而UDP是不面向连接的,传输数据之前不需要建⽴连接,如写信直接发出去了。
·TCP提供可靠的通信,即TCP会保证数据的不丢包、不重复、且有序的传输;而UDP会尽可能地保证数据传输的可靠性,但⽆法确保数据传输不丢包。
·每⼀个TCP连接仅⽀持点对点的通信;⽽⼀个UDP连接可以⽀持⼀对⼀、⼀对多和多对多等多种通信⽅式。
三,TCP/IP示例程序
服务器端(tcp_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080 // 定义端口号
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建 Socket 文件描述符 (使用 SOCK_STREAM 代表 TCP)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket 创建失败");
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("绑定失败");
exit(EXIT_FAILURE);
}
// 3. 开始监听端口 (等待客户端连接)
if (listen(server_fd, 3) < 0) {
perror("监听失败");
exit(EXIT_FAILURE);
}
printf("TCP 服务器正在监听端口 %d...\n", PORT);
// 4. 接受客户端连接 (这是一个阻塞函数,会一直等到有客户端连接进来)
if ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("接受连接失败");
exit(EXIT_FAILURE);
}
printf("客户端已连接。\n");
// 5. 接收来自客户端的消息
read(client_socket, buffer, BUFFER_SIZE);
printf("收到客户端消息: %s\n", buffer);
// 6. 向客户端发送回复消息
const char *response = "消息已收到!";
send(client_socket, response, strlen(response), 0);
printf("已向客户端发送回复。\n");
// 7. 关闭连接
close(client_socket);
close(server_fd);
return 0;
}
客户端(tcp_client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "你好,我是TCP客户端!";
// 1. 创建 Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket 创建错误 \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将IP地址从字符串转换为网络格式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\n无效的地址/不支持的地址 \n");
return -1;
}
// 2. 连接到服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\n连接失败 \n");
return -1;
}
// 3. 发送消息到服务器
send(sock, message, strlen(message), 0);
printf("消息已发送: %s\n", message);
// 4. 接收服务器的回复
read(sock, buffer, BUFFER_SIZE);
printf("收到服务器回复: %s\n", buffer);
// 5. 关闭连接
close(sock);
return 0;
}
运行编译程序:
四,UDP示例程序
UDP 服务器 (udp_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8081 // 使用不同于TCP的端口
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr, cliaddr;
// 1. 创建 Socket (使用 SOCK_DGRAM 代表 UDP)
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
// 2. 绑定服务器地址和端口
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("绑定失败");
exit(EXIT_FAILURE);
}
printf("UDP 服务器正在监听端口 %d...\n", PORT);
socklen_t len = sizeof(cliaddr);
// 3. 接收来自客户端的数据报 (recvfrom 会同时获取数据和客户端地址)
recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
buffer[strlen(buffer)] = '\0'; // 确保字符串结束
printf("收到客户端消息: %s\n", buffer);
// 4. 向客户端发送回复
const char *response = "UDP 消息已收到!";
sendto(sockfd, (const char *)response, strlen(response), 0, (const struct sockaddr *) &cliaddr, len);
printf("已向客户端发送回复。\n");
// 5. 关闭socket
close(sockfd);
return 0;
}
UDP 客户端 (udp_client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT 8081
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
const char *message = "你好,我是UDP客户端!";
struct sockaddr_in servaddr;
// 1. 创建 Socket
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
// 2. 填写服务器地址信息 (不需要 connect)
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 3. 发送消息到服务器 (使用 sendto)
sendto(sockfd, (const char *)message, strlen(message), 0, (const struct sockaddr *) &servaddr, sizeof(servaddr));
printf("消息已发送: %s\n", message);
// 4. 接收服务器的回复 (使用 recvfrom)
socklen_t len;
recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *) &servaddr, &len);
buffer[strlen(buffer)] = '\0'; // 确保字符串结束
printf("收到服务器回复: %s\n", buffer);
// 5. 关闭 socket
close(sockfd);
return 0;
}
编译运行程序: