TCP/IP (Transfer Control Protocol/Internet Protocol) 协议,我们也叫做⽹络通讯协议。包含了⼀系列构成互联⽹基础的⽹络协议,是 Internet 的核⼼协议。
国际标准化组织(ISO)制定了一个用于计算机或通信系统间互联的标准体系,一般称为 OSI 参考模型或七层模型;
而与工业生产中实际用到的 TCP/IP 五层模型相比。这两者者的关系就好比,车间里面一个贴在墙上很规范的流程图,一个是实际生产中因为成本/便捷/效率等因素最终采用的流程。
(还有一种说法,将物理层与数据链路层合并称为网络接入层,这种情况下 TCP/IP 为四层模型)
但是不管是七层模型还是五层模型,他们实现的目无非是为了解决两个问题:
- 数据转成电信号;
- 目标主机的寻址。
其中,传输层有 TCP/UDP 两种连接方式,所以对应的 Socket 也有两种不同实现方式,掌握 Socket 的前提是了解清楚这两种协议。
在 TCP/IP 协议中,TCP 协议和 IP 协议分别完成不同的任务。
TCP 是⽤来检测⽹络传输中的差错。
IP 协议可以将多个交换⽹络连接起来,在源地址和⽬的地址之间传送数据包。同时,它还提供数据重新组装功能,以适应不同⽹络对数据包⼤⼩的要求。
在 TCP/IP 协议中,使⽤ IP 协议传输的数据包就是 IP 数据包。 IP 报⽂是在⽹络层传输的数据单元,也叫 IP 数据报,IP数据报由首部与数据部分组成。
TCP协议是面向连接,且具备顺序控制和重发机制的可靠传输。他的可靠性是在于传输数据前要先建立连接,确保要传输的对方有响应才进行数据的传输。因此 TCP 有个经典的 3 次握手和 4 次挥手。
握手的目的是为了相互确认通信双方的状态都是正常的,没有问题后才会进行正式的通信:
3 次握手的整个过程看着似乎有点过于谨慎,但是互联网的初期网络基础设施是很落后的,丢包的概率非常大的。而且这个过程也只是在通信前期建立连接的时候进行,3 次握手过后就是正常的消息传输了。
4 次挥手的目的跟 3 次握手目的是一样的,确保双方消息状态的准确:
UDP 是一种不可靠的传输机制,但是它的数据报文比 TCP 小,所以相同数据的传输 UDP 所需的带宽更少,传输速度更快。它不要事先建立连接,知道对方的地址后直接数据包就扔过去,也不保证对方有没有收到。
UDP 数据报主要由两个部分组成:⾸部 + 数据部分。
⾸部部分很简单,只有 8 个字节,由四个字段组成,每个字段的⻓度都是两个字节。
字段含义 :
特点:
性能:
使用场景:
TCP/IP 五层⽹络模型的
应⽤层编程接⼝
称为 Socket API,Socket(套接字) 本身有 “插座” 的意 思,它是对⽹络中不同主机上的应⽤进程之间进⾏双向通信的端点的抽象。 ⼀个套接字就是⽹络上进程通信的⼀端,提供了应⽤层进程利⽤⽹络协议交换数据的机制。从所处的地位来讲,套接字上联应⽤进程,下联⽹络协议栈,是应⽤程序通过⽹络协议进⾏通信的接⼝。
Socket 套接字类型
- 流式套接字 (SOCKET_STREAM) 提供了⼀个⾯向连接、可靠的数据传输服务,数据⽆差错、⽆重复的发送且 按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收⽅。数据被看作是字节流,⽆⻓度限制。
- 数据报套接字 (SOCK_DGRAM) 提供⽆连接服务。数据包以独⽴数据包的形式被发送,不提供⽆差错保证, 数据可能丢失或重复,顺序发送,可能乱序接收。
- 原始套接字 (SOCK_RAW) 可以对较低层次协议如 IP、ICMP 直接访问。
因为在网络上传输的数据都是以字节流的形式进行传输,所以在正式传输之前,我们需要将IP字符串转换为网络字节串,这样子才能告诉协议们建立连接时双方的IP地址,确保找到正确的传输目标。
方法1:调用inet_addr函数
#include
#include
#include
typedef unsigned int uint32_t;
typedef unsigned int in_addr_t;
in_addr_t inet_addr(const char *cp);
功能:将cp指向的IP字符串转成⽹络字节序
返回值: 成功返回⽹络字节序,失败返回INADDR_NONE [0xffffffff]
注意:它不能识别255.255.255.255
方法2:调用inet_aton函数
int inet_aton(const char *cp, struct in_addr *inp);
功能:将cp指向的IP字符串转成⽹络字节序并保存到inp的地址中。
参数:
@cp IP字符串⾸地址
@inp 存放⽹络字节序的地址
返回值: 成功返回⾮0,失败返回0
struct in_addr
{
unsigned int s_addr;
};
实质:将网络字节序存储在inp结构体指针中的 s_addr这个变量中。
方法:调用inet_ntoa函数
char *inet_ntoa(struct in_addr in);
功能:将IP⽹络字节序转换成IP字符串
参数:
@in IP⽹络字节序
返回值: 成功返回IP字符串⾸地址,失败返回NULL
主机字节序是什么?
我们知道,计算机数据存放到内存中有两种存储方式:
因为存储方式的不同,我们将使用小端存储方式的网络字节序称为主机字节序。
而网络字节序采用大端存储形式。
short htons(short data);
功能:将short类型的整数从主机字节序转成⽹络字节序
参数:
@data 序号转换的整数
返回值:得到的⽹络字节序
以int类型的1为例,1在内存中存储的大小端格式如下:
如果我们可以得到1在内存中存储的第一个字节,那么我们就可以知道当前系统是大端存储还是小端存储了。
程序如下:
#include
int main()
{
int a = 1;
char pc = *(char*)(&a);
if (pc == 1)
printf("第一个字节为1,小端存储\n");
else
printf("第一个字节为0,大端存储\n");
return 0;
}
int atoi(const char *nptr);
功能:把ntpr 所指向的整数字符串转换成整数。
参数:
@ nptr 字符串
返回值:成功,返回转换后的整数
失败,返回0
注意:若是只有+,-和整数字符则能正常转换,其他字符返回0
-------------------------------------------------------------------------
uint32_t ntohs(uint32_t netlong); [network to host short]
功能:把⽹络字节序转换为主机端⼝
参数:
@ netlong ⽹络字节序
返回值: 返回对应的主机端⼝
UDP 是⼀个传输层的⽆连接的协议,我们编写代码⼀般是分为两个端。⼀个我们称之为发送端,另⼀ 个我们称之为接收端。正常⼀般是接收端先运⾏,然后等待结束发送端发送过来的数据。
编写UDP发送端分为两个步骤
创建套接字需调用socket函数
int socket(int domain, int type, int protocol);
参数:
@domain 地址族
AF_UNIX 本地unix域通信
AF_INET IPV4 ineter⽹通信 [我们使⽤这个]
@type 使⽤协议类型
SOCK_STREAM 流式套接字(TCP)
SOCK_DGRAM 报⽂套接字(UDP)
SOCK_RAW 原始套接字: (IP,ICMP)
@protocol 协议编号
0 : 让系统⾃动识别
返回值: 成功返回得到的⽂件描述符。
失败返回 -1
-------------------------------------------------------------------------
示例用法:
int fd = socket(AF_INET,SOCK_DGRAM,0);
发送数据需调用sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
@sockfd 套接字
@buf 数据存放的⾸地址
@len 期望发送的数据⼤⼩
@flags 操作⽅式 (0 表示默认操作)
@dest_addr 向指定的地址发送数据
@addrlen 发送的地址的⼤⼩
返回值:
成功返回实际发送的字节数,失败返回-1
-------------------------------------------------------------------------
对于 struct sockaddr *dest_addr
Linux操作系统内置了两种结构体可供选择,用来处理网络通信的地址:
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
//sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了
};
//该结构体解决了sockaddr的缺陷,把 port和addr 分开储存在两个变量中
struct sockaddr_in {
short int sin_family; //地址族 (Address Family)
//AF_INET 表示IPv4网络协议
//AF_INET6 表示IPv6
unsigned short int sin_port; //16位的端口号
struct in_addr sin_addr; //32位的IP地址
unsigned char sin_zero[8]; //暂时没什么卵用
};
struct in_addr {
uint32_t s_addr; //32位的IPv4地址
};
创建一个UDP通信发送方的全部代码如下:
//创建UDP通信
//UDP发送方
#include
#include
#include
#include
#include
#include
#include
void send_data(int sockfd, struct sockaddr_in *addr, int len)
{
int n = 0;
char buf[1024] = {0}; //数据区
while(1){
putchar('>');
memset(buf, 0, sizeof(buf));
//用户输入
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0'; // 将 \n ---> \0
//发送
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)addr, len);
if(n < 0){
perror("Fail to sendto");
exit(EXIT_FAILURE);
}
if(strncmp(buf, "quit", 4) == 0){
break;
}
}
}
//运行形式: ./a.out ip port
int main(int argc, const char *argv[])
{
if(argc != 3){
fprintf(stderr, "Usage : %s ip port!\n", argv[0]);
exit(EXIT_FAILURE);
}
//1.通过socket创建文件描述符
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
perror("Fail to socket!");
exit(EXIT_FAILURE);
}
//2.填充服务器的ip port
struct sockaddr_in peer_addr;
memset(&peer_addr, 0, sizeof(peer_addr)); //凊空杂数据
peer_addr.sin_family = AF_INET; //指定协议族
peer_addr.sin_port = htons(atoi(argv[2])); //端口号
peer_addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址
//3.发送数据
int len = sizeof(peer_addr);
send_data(sockfd, &peer_addr, len);
//4.关闭文件描述符
close(sockfd);
return 0;
}
调用bind函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
功能:把ip地址和端⼝绑定到socket中去。
参数:
@sockfd socket创建的⽂件描述符
@addr 把IP和地址设置到对应的结构体中去。
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr
{
unsigned int s_addr;
}
@addrlen 表示 addr 参数对应类型的地址信息结构体的⼤⼩
返回值: 成功 返回0;失败返回 -1 ,并设置errno
示例用法:
1)定义结构体
struct sockaddr_in my_addr;
memest(&my_addr,0,sizeof(my_addr));
2)填充数据
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[2]));
my_addr.sin_addr.s_addr = inet_addr(argv[1]);
3)绑定数据
if(bind(sockfd,(struct sockaddr *)&my_addr),sizeof(my_addr) < 0)
{
...
}
调用recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen)
参数:
@sockfd 套接字
@buf 数据存放的⾸地址
@len 期望接收的数据⼤⼩
@flags 操作⽅式 0 表示默认操作
@src_addr 获得发送⽅地址,谁发送的获得谁的地址。
@addrlen 值结果参数,必须进⾏初始化, 表示表示对⽅实际地址的⼤⼩。
返回值:
成功返回实际接收的字节数,失败返回-1
示例用法:
struct sockaddr_in peer_addr;
socklen_t addrlen = sizeof(struct sockaddr_in);
n = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&peer_addr,&addrlen);
创建一个UDP接收方的全部代码如下:
//创建UDP通信
//UDP接收方
#include
#include
#include
#include
#include
#include
#include
void recv_data(int sockfd)
{
int n = 0;
char buf[1024] = {0};
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
while(1){
puts("--------------------");
memset(buf, 0, sizeof(buf));
n = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);
if(n < 0){
perror("Fail to recvfrom");
exit(EXIT_FAILURE);
}
printf("Recv from IP = %s\n", inet_ntoa(client_addr.sin_addr));
printf("Recv from Port = %d\n", ntohs(client_addr.sin_port));
printf("Recv %d bytes : <%s>\n", n, buf);
if(strncmp(buf, "quit", 4) == 0){
break;
}
}
}
int main(int argc, const char *argv[])
{
if(argc != 3){
fprintf(stderr, "Usage : %s ip port!\n", argv[0]);
exit(EXIT_FAILURE);
}
//1.通过socket创建文件描述符
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
perror("Fail to socket!");
exit(EXIT_FAILURE);
}
//2.填充服务器自己的ip port
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET; //协议簇
my_addr.sin_port = htons(atoi(argv[2])); //端口号
my_addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址
//3.把ip port与socket绑定
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0){
perror("Fail to bind");
exit(EXIT_FAILURE);
}
printf("wait recv data...\n");
//3.接收数据
recv_data(sockfd);
//4.关闭文件描述符
close(sockfd);
return 0;
}