学到Linux网络通信这部分,整理一下笔记,从基础概念到UDP编程都过一遍。
一、网络协议基础
网络通信说白了就是不同主机上的进程之间通信。
OSI七层模型
这是个理论模型,1977年国际标准化组织搞的,目的是让不同体系结构的计算机能互联成网。从上到下七层:
-
应用层:要传输的数据信息,比如文件传输、电子邮件
-
表示层:数据加密解密、压缩解压
-
会话层:建立数据传输通道
-
传输层:传输方式(UDP/TCP)、端口号
-
网络层:数据路由、路径规划,对应路由器和IP
-
数据链路层:封装成帧、点对点通信、差错检测,对应交换机和ARP
-
物理层:定义物理设备标准,传输比特流(0和1)
TCP/IP模型
实际生产用的是这个。四层模型把应用层、表示层、会话层合并为应用层,数据链路层和物理层合并为网络接口层。也有五层的分法,各层常见协议:
-
应用层:HTTP(超文本传输)、HTTPS(SSL加密)、FTP(文件传输,TCP)、TFTP(简单文件传输,UDP)、DNS(域名解析)、MQTT(物联网消息队列遥测传输)
-
传输层:TCP(传输控制协议)、UDP(用户数据报协议)
-
网络层:IPv4、IPv6
-
数据链路层:MAC与IP转换,ARP(地址解析)、RARP(逆地址解析)
-
物理层
二、IP协议
IPv4是32位,IPv6是128位。我们平时看到的192.168.1.3这种叫点分十进制,计算机里存的是32位二进制。
IP地址 = 网络位 + 主机位。比如192.168.0.121/24,这个24就是网络位的位数。
子网掩码用来区分网络位和主机位,比如255.255.255.0,是1的部分对应网络位,是0的部分对应主机位。
几个相关概念:
-
网段号:网络位不变,主机位全为0。比如192.168.1.3配255.255.0.0,网段号就是192.168.0.0
-
广播号:网络位不变,主机位全为1。向广播号发信息,局域网内所有IP都能收到
-
网关地址:比如192.168.1.1
IP地址分五类:
-
A类:1.0.0.0 - 126.255.255.255,子网掩码255.0.0.0,管理大规模网络。私有地址10.0.0.0-10.255.255.255。127.0.0.0是回环地址
-
B类:128.0.0.0 - 191.255.255.255,子网掩码255.255.0.0,管理大中规模网络。私有地址172.16.0.0-172.31.255.255
-
C类:192.0.0.0 - 223.255.255.255,子网掩码255.255.255.0,管理中小规模网络。私有地址192.168.0.0-192.168.255.255
-
D类:224.0.0.0 - 239.255.255.255,组播和广播用
-
E类:240.0.0.0 - 255.255.255.254,实验用
公有IP由电信公司分配、需要付费,可以直接访问互联网。私有IP不能直接访问互联网,主要是为了节省IP地址。
三、端口号
IP+PORT才能找到目标主机上的目标进程。端口号是16位的数值,范围0-65535,作用是唯一标识一个网络进程,通讯时用来区分数据包属于哪个进程。
分三类:
-
1-1023:知名端口,http是80,FTP是20/21,TFTP是69,HTTPS是443,MQTT是1883
-
1024-49151:被IANA注册的端口,指定给特殊服务
-
49152-65535:动态或私有端口
四、网络配置
虚拟机用桥接模式联网的步骤:
-
虚拟机设置 → 网络适配器 → 桥接模式 → 确定
-
编辑 → 虚拟网络编辑器 → 更改设置 → VMnet0 → 桥接模式 → 桥接到Windows正在上网的网卡 → 应用确定
-
修改配置文件:sudo vim /etc/network/interfaces
配置内容大概这样:
plaintext
auto lo
iface lo inet loopback
auto ens33
iface ens33 inet dhcp
-
重启网络服务:sudo /etc/init.d/networking restart,不行就sudo reboot
-
ifconfig查看Linux网卡信息,Windows下是ipconfig
-
ping www.baidu.com检查网络通不通
五、UDP协议
UDP是用户数据报协议(User Datagram Protocol),在传输层。
网络通信模型
两种:
-
B/S(Browser/Server):浏览器/服务器。通用客户端,数据资源基本存在服务端,只需要开发服务端
-
C/S(Client/Server):客户端/服务端。专用客户端,两端都可以存资源,两端都需要开发
UDP编程相关函数
socket
int socket(int domain, int type, int protocol);
创建通信的套接字。参数:
-
domain:协议族,AF_INET是IPv4,AF_INET6是IPv6
-
type:传输层协议类型,SOCK_STREAM是TCP,SOCK_DGRAM是UDP
-
protocol:0默认方式
返回值:成功返回文件描述符(套接字),失败返回-1。套接字就是应用层网络通信的端口,本质是个文件描述符。
sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
发送UDP数据报文。参数:
-
sockfd:套接字
-
buf:要发送的数据首地址
-
len:发送的数据字节数
-
flags:0默认方式
-
dest_addr:服务端地址信息(IP+PORT)
-
addrlen:地址信息长度
返回值:成功返回发送的字节数,失败返回-1。
地址结构体
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 网络字节序的端口 */
struct in_addr sin_addr; /* IP地址 */
};
struct in_addr {
uint32_t s_addr; /* 网络字节序的地址 */
};
字节序
网络字节序是大端(高地址存低字节位,低地址存高字节位),主机字节序一般是小端。转换函数:
-
htons:2字节,主机转网络
-
htonl:4字节,主机转网络
-
ntohs:2字节,网络转主机
-
ntohl:4字节,网络转主机
h是host,n是network,s是short(2字节),l是long(4字节)。
inet_addr
in_addr_t inet_addr(const char *cp);
把点分十进制的字符串IP转换成32位二进制IP地址形式。参数是字符串IP,返回整型IP地址。
bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
绑定套接字和IP地址及端口号。成功返回0,失败返回-1。
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
接收UDP数据报文。参数:
-
sockfd:套接字
-
buf:存储接收到的数据
-
len:期望接收的字节数
-
flags:0默认方式
-
src_addr:保存发送方地址信息的指针
-
addrlen:地址长度的指针
返回值:成功返回实际接收的字节数,失败返回-1,返回0表示对方发了0字节数据包。
inet_ntoa
char *inet_ntoa(struct in_addr in);
把结构体类型的IP地址转换成字符串形式的IP地址。
UDP客户端示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.119");
char buff128 = {0};
fgets(buff, sizeof(buff), stdin);
buffstrcspn(buff, "\\n") = '\0';
ssize_t size = sendto(sockfd, buff, strlen(buff), 0,
(struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == size) {
perror("sendto error");
close(sockfd);
return -1;
}
close(sockfd);
return 0;
}
UDP服务端示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == sockfd) {
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.119");
if (bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)) == -1) {
perror("bind error");
close(sockfd);
return -1;
}
char buff128 = {0};
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
ssize_t size = recvfrom(sockfd, buff, sizeof(buff), 0,
(struct sockaddr *)&cliaddr, &addrlen);
if (-1 == size) {
perror("recvfrom error");
close(sockfd);
return -1;
}
printf("收到来自 %s 的数据: %s\n", inet_ntoa(cliaddr.sin_addr), buff);
close(sockfd);
return 0;
}
UDP不需要建立连接,客户端直接sendto,服务端bind后recvfrom等着收就行,这也是它和TCP最大的区别。