目录
12.1 概念:同样也是基于setsockopt实现的文件属性设置
IP地址与网络通信基础详解
1. IP地址特点
- IP地址是互联网中唯一标识主机的地址,采用二进制形式表示。
- 主机间通信需具备IP地址,用于区分不同主机。
- 分为IPv4(32位)和IPv6(128位)。
- 每个数据包需携带源IP和目标IP信息。
- 表示方法为点分十进制,由四个0-255的十进制数组成,每组对应8位二进制。
2. IP地址分类
2.1 普通地址
- A类:范围0.0.0.0-127.255.255.255,子网掩码255.0.0.0。
- B类:范围128.0.0.0-191.255.255.255,子网掩码255.255.0.0。
- C类:范围192.0.0.0-223.255.255.255,子网掩码255.255.255.0。
- D类:范围224.0.0.0-239.255.255.255,用于多播。
- E类:保留地址,范围240.0.0.0-255.255.255.255。
2.2 特殊地址
- 0.0.0.0:匹配本机所有IPv4地址。
- 127.0.0.1:回环地址,用于本机测试。
- 网络地址:主机号全为0的地址,如192.168.50.0。
- 广播地址:主机号最大的地址,用于向同一网段内所有主机发送信息。
- 全网广播地址:255.255.255.255,慎用,可能影响整个网络。
3. 子网掩码
3.1 特点
- 与IP地址长度相同,共32位。
- 网络号全为1,主机号全为0。
3.2 作用
- 划分IP地址的网络部分和主机部分。
4. 地址划分
- 二级地址:IP = 网络号 + 主机号
- 三级地址:引入子网掩码,细化网络划分。
5. 网络模型
5.1 OSI模型
- 物理层:转换二进制为电信号。
- 数据链路层:封装数据为帧,进行错误检测。
- 网络层:负责IP寻址。
- 传输层:标识进程,通过端口号寻址。
- 会话层:建立和维护主机间通信。
- 表示层:数据加密和格式化。
- 应用层:提供各种应用服务。
编辑
5.2 TCP/IP模型
- 应用层:应用程序和协议集合。
- 传输层:端口寻址,决定数据流向。
- 网络层:IP寻址。
- 网络接入层:实现物理和数据链路层功能,提供统一接口。
UDP与TCP传输协议及Socket套接字详解
6. UDP与TCP对比
6.1 共同点
- 位于传输层,支持全双工通信。
6.2 TCP传输协议
- 面向连接,提供可靠通信。
- 数据传输过程中若网络中断,需重新开始。
- 适用于对数据可靠性要求高的场景,如QQ、微信的登录或密码修改。
6.3 UDP传输协议
- 无连接,数据传输不可靠。
- 网络中断后可继续传输未完成部分。
- 适用于实时性要求高但对数据丢失容忍度高的场景,如QQ、微信视频通话。
7. Socket套接字
7.1 概念
- 编程接口,用于网络通信。
- 特殊的文件描述符,用于数据收发。
- 不仅限于TCP/IP协议,支持多种通信机制。
- 包括面向连接和无连接两种类型。
7.2 功能
- 内核中创建接收和发送缓存区。
- 提供用户空间访问内核空间的文件描述符。
7.3 类型
- 流式套接字:面向连接,保证数据的可靠、无差错传输。
- 数据报套接字:无连接,数据传输不可靠,适用于不需要保证数据完整性的场景。
- 原始套接字:底层协议访问,灵活性高,但使用复杂。
7.4 端口号
- 用于区分主机上的不同进程,确保数据包正确传递。
- TCP与UDP端口号独立,允许同一端口号用于不同协议。
- 端口号由IANA管理,用于标准化服务分配。
- 占用2字节,与IP地址共同定位网络中的具体进程。
7.5 字节序
- 小端模式:低地址存储低位字节。
- 大端模式:低地址存储高位字节。
端口分配
- 众所周知端口(1~1023):系统级服务使用,如TFTP端口69。
- 已登记端口(1024~49151):常用服务端口,用于建立会话。
- 动态或私有端口(49152~65535):临时分配,用于特定服务或私有应用。
编辑
7.6 端口号字节序转换
- 网络传输采用网络字节序,终端输入输出使用主机字节序。
- 主机字节序到网络字节序转换函数:
htonl
: 转换32位长整型(u_long
)数据。htons
: 转换16位短整型(u_short
)数据,通常用于端口号转换。
- 网络字节序到主机字节序转换函数:
ntohl
: 转换32位长整型数据。ntohs
: 转换16位短整型数据,适用于端口号转换。
- 头文件:
#include <arpa/inet.h>
7.7 IP地址字节序转换
inet_addr()
函数用于将点分十进制的IP地址(主机字节序)转换为网络字节序。- 头文件:
#include <sys/socket.h>
、#include <netinet/in.h>
、#include <arpa/inet.h>
- 数据类型定义:
typedef uint32_t in_addr_t;
- 结构体定义:
struct in_addr { in_addr_t s_addr; };
- 函数原型:
in_addr_t inet_addr(const char *strptr);
- 功能:将点分十进制的IP地址字符串转换为网络字节序的无符号32位整型数。
- 参数:
const char *strptr
为点分十进制IP地址字符串。 - 返回值:成功转换返回无符号32位整型数,转换失败返回
INADDR_NONE
。
编辑
编辑
7.8 TCP编程
7.8.1 socket 创建套接字
int socket(int domain, int type, int protocol); 头文件: #include <sys/types.h> #include <sys/socket.h> 功能:创建套接字 参数: domain:协议族 AF_UNIX, AF_LOCAL 本地通信 AF_INET ipv4 AF_INET6 ipv6 type:套接字类型 SOCK_STREAM:流式套接字 SOCK_DGRAM:数据报套接字 protocol:协议 - 填0 自动匹配底层 ,根据type 系统默认自动帮助匹配对应协议 传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP 网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL) 返回值: 成功 文件描述符 0 -> 标准输入 1->标准输出 2->标准出错 3->socket 失败 -1,更新errno
7.8.2 bind 绑定套接字
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 头文件: #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<netinet/ip.h> 功能:绑定 协议? ip? 端口 ? 让别人识别 参数: sockfd:套接字 addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,填充对应结构体-通信当时socket第一个参数确定,需要强转) (结构体之间的强转,不可以直接使用普通数据类型强转,因为结构体字节对齐原则,具体大小不一致,会数据丢失,所以可使用结构体指针,对结构体地址进行强转) addrlen:结构体大小 返回值:成功 0 失败-1,更新errno 通用结构体: struct sockaddr { sa_family_t sa_family; char sa_data[14]; } ipv4通信结构体: struct sockaddr_in { sa_family_t sin_family; ----协议族 in_port_t sin_port; ----端口 struct in_addr sin_addr; ----ip结构体 }; struct in_addr { uint32_t s_addr; --ip地址 }; 本地通信结构体: struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX 本地通信 */ char sun_path[108]; /* 在本地创建的套接字路径 */ };
7.8.3 listen 监听
int listen(int sockfd, int backlog); 功能:监听,将主动套接字变为被动套接字 参数: sockfd:套接字 backlog:同一时间可以响应客户端请求链接的最大个数,不能写0. 不同平台可同时链接的数不同,一般写6-8个 返回值:成功 0 失败-1,更新errno
7.8.4 accpt 阻塞等待连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 功能:阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接, 则accept()函数返回,返回一个用于通信的套接字文件; 参数: Sockfd :套接字 addr: 链接客户端的ip和端口号 如果不需要关心具体是哪一个客户端,那么可以填NULL; addrlen:结构体的大小 如果不需要关心具体是哪一个客户端,那么可以填NULL; 返回值: 成功:文件描述符; //用于通信 失败:-1,更新errno新errno
7.8.5 recv 接受缓存区的内容
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 功能: 接收数据 参数: sockfd: acceptfd ; buf 存放位置 len 大小 flags 一般填0,相当于read()函数 MSG_DONTWAIT 非阻塞 返回值: < 0 失败出错 更新errno ==0 表示客户端退出 >0 成功接收的字节个数
7.9 UDP 编程
7.9.1 函数接口(recvfrom sendto)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 功能:接收数据 参数: sockfd:套接字描述符 buf:接收缓存区的首地址 len:接收缓存区的大小 flags:0 接收数据 并 阻塞 MSG_DONTWAIT: 设置非阻塞 src_addr: 发送端的网络信息结构体的指针(对方的 caddr) addrlen:发送端的网络信息结构体的大小的指针(对方的 caddr) 返回值: 成功接收的字节个数 接收到的数据为0 : 0 失败:-1 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 发送消息并阻塞 src_addr:接收端的网络信息结构体的指针 addrlen:接收端的网络信息结构体的大小 返回值: 成功发送的字节个数 失败:-1
服务器模型
9.1 循环服务器
【1】 循环服务器 (同一时刻,只能连接一个客户端,进行通信)
特点: 循环服务器,同一时刻,只能处理一个客户端请求;
缺点: 循环处理客户端, 不能做耗时动作(一直循环一直工作);
TCP服务器流程:
socket()
bind();
listen()
while(1)
{
accept()
while(1)
{
recv();
}
close(accpetfd);
}
UDP服务器流程:
socket();
bind();
while(1)
{
recvfrom();
}
close();
9.2 并发服务器
1.多进程实现并发(最好不用)
socket()
bind();
listen();
while(1)
{
accept();
if(fork() == 0) //子进程
{
while(1)
{
process();
}
close(client_fd);
exit();
}
else
{
}
}
2. 线程实现
每来一个客户端连接, 开一个子线程来专门处理客户端的数据, 实现简单, 资源占用少;
socket()
bind();
listen();
while(1)
{
accept();
pthread_create();
}
函数接口:
创建线程
#include<pthread.h>
原型:
int pthread_create
(pthread_t *thread , const pthread_attr_t*arr , void*(* routine)(void *) , void *arg)
功能: 创建线程
参数: pthread_t *thread: 指针,要指向一个地址。创建的线程对象,地址
pthread_attr_t: 线程的属性,为NULL表示默认属性
void*(* routine)(void *): 线程函数(函数指针),传入函数名
void*arg: 给线程函数传参,以地址的形式,不需要传参可以设NULL
返回值: 成功 : 0 失败 : err no
退出线程
格式:
Int pthread_exit(void *retval)
功能: 用于退出线程的执行
参数: retval: 线程退出时返回的值
返回值: 成功: 0
失败: errno
使用: pthread_exit(NULL);
将线程设置为游离态,自动回收线程资源,不阻塞
pthread_detach
int pthread_detach(pthread_t thread);
功能:将线程设置为游离态,等线程退出系统自动回收线程资源
参数:
thread:线程tid
返回值:成功0,失败非0
使用: pthread_detach(tid);
线程实现并发的代码
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> #include <sys/wait.h> #include <pthread.h> void *mythread(void *arg)//int acceptfd { //先把void * 强转为 int *, 然后取该指针的值 int acceptfd = *((int *)arg); //acceptfd char buf[128] = ""; int recvbyte; while(1) { recvbyte = recv(acceptfd,buf,sizeof(buf),0); if(recvbyte < 0) { perror("recv is err:"); return NULL; } else if(recvbyte == 0) { printf("client is exit\n"); break; } else { printf("%s\n",buf); } } close(acceptfd); //线程退出函数 pthread_exit(NULL) ;} int main(int argc, char const *argv[]) { if(argc != 2) { printf("please input %s <port>",argv[0]); return -1; } //1.创建流式套接字 int sockfd = socket(AF_INET,SOCK_STREAM,0); if(sockfd < 0) { perror("sockfd is err:"); return -1; } //2.填充ipv4结构体 struct sockaddr_in saddr,caddr; saddr.sin_family = AF_INET; saddr.sin_port = htons(atoi(argv[1])); saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); int len = sizeof(caddr); if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) < 0) { perror("bind is err:"); return -1; } //3.监听 if(listen(sockfd,5)<0) { perror("listen is err:"); return -1; } while(1) { int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len); if(acceptfd < 0) { perror("accept is err:"); return -1; } printf("client: ip %s port %d\n",\ inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); pthread_t tid; pthread_create(&tid,NULL,mythread,&acceptfd); //将子线程设置为游离态, 不阻塞等待该线程的退出; //一旦该线程退出, 则会自动回收自己的资源 pthread_detach(tid); } close(sockfd); return 0; }
并发服务器总结:
多进程:
优点: 服务器更稳定 , 父子进程资源独立, 安全性高一点
缺点: 消耗资源
多线程:
优点: 资源开销小, 线程共享同一个进程的资源
缺点: 安全性较差
IO多路复用:
优点: 节省资源, 减小系统开销,性能高;
缺点: 代码复杂性高
组播 -UDP
12.1 概念:同样也是基于setsockopt实现的文件属性设置
12.2 组播的地址
二级划分 D类IP: 224.0.0.1 - 239.255.255.255
//多播结构体
struct ip_mreq{
struct in_addr imr_multiaddr; //指定多播组IP
struct in_addr imr_interface; //本地IP,通常指定为 INADDR_ANY--0.0.0.0
}
struct in_addr{
_be32 s_addr; //IP地址(大端)
}
//核心代码 ------------------------------------
struct ip_mreq mreq;
bzero(&mreq, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.1"); //填充多播组IP
mreq.imr_interface.s_addr = inet_addr("0.0.0.0"); //自动获取本机IP
//改变套接字属性
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
12.3 组播的客户端
1.创建套接字
2.加入多播组
3.接收者绑定组播IP地址
4.等待接收数据
简易代码实现
socket()
struct ip_mreq mreq; //组播的结构体变量
mreq.imr_multiaddr.s_addr = inet_addr(argv[1]); //组IP
mreq.imr_interface.s_addr = inet_addr("0.0.0.0"); //本地IP
setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));
//3. 填充结构体: 绑定组播ip和端口
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //组IP
socklen_t len = sizeof(caddr);
12.4 组播的服务器
1.创建数据报套接字
2.指定接收方地址为 组播地址 如 224.0.0.10 以及端口信息
3.发送数据包
- 广播和组播的区别
广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响通信。
组播(又称为多播)是一种折中的方式。只有加入某个多播组的主机才能收到数据。
组播方式既可以发给多个主机,又能避免像广播那样带来过多的负载
本地套接通信字
14.1 特性
· unix网络编程最开始有的编程都是一台主机内进程和进程之间的编程。(本地通信) · socket可以用于本地间进程通信,创建套接字时使用本地协议AF_LOCAL或AF_UNIX 本地通信不需要ip和端口,无法进行两个主机通信 · 分为流式套接字和数据报套接字 //可以使用流式套接字或者数据包套接字 · 和其他进程间通信相比使用方便、效率更高,常用于前后台进程通信。 · unix域套接字编程,实现本间进程的通信,依赖的是s类型的文件; 核心流程 #include <sys/socket.h> #include <sys/un.h> struct sockaddr_un { sa_family_t sun_family; /* 本地协议 AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* 本地路径 s类型的套接字文件 */ }; unix socket = socket(AF_UNIX, type, 0); //type可以为流式套接字或数据包套接字 //unix 写为 int 就可以 struct sockaddr_un myaddr; myaddr.sun_family = AF_UNIX; //填充UNIX域套接字 strcpy(saddr.sun_path,"./myunix"); // 创建套接字的路径 recv代码 #include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <stdlib.h> #include <stdio.h> #include <poll.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <string.h> int main(int argc, char const *argv[]) { //1.创建套接字 int sockfd = socket(AF_UNIX,SOCK_STREAM,0); if(sockfd < 0) { perror("sock is err:"); return -1; } system("rm ./myunix -f");// 两者都是删除地址的,二选一即可 unlink("./myunix"); //2. 填充结构体 struct sockaddr_un saddr; saddr.sun_family = AF_UNIX; strcpy(saddr.sun_path,"./myunix"); if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) < 0) { perror("bind is err:"); return -1; } if(listen(sockfd,5) < 0) { perror("listen is err:"); return -1; } int acceptfd = accept(sockfd,NULL,NULL); if(acceptfd < 0) { perror("acceptfd < 0"); return -1; } char buf[128] = ""; int recvbyte; while(1) { recvbyte = recv(acceptfd,buf,sizeof(buf),0); if(recvbyte < 0) { perror("recv is err:"); return -1; } else if(recvbyte == 0) { printf("exit\n"); break; } else { printf("buf: %s\n",buf); } } close(sockfd); close(acceptfd); return 0; } send代码: #include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <stdlib.h> #include <stdio.h> #include <poll.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <arpa/inet.h> #include <string.h> int main(int argc, char const *argv[]) { //1.创建套接字 int sockfd = socket(AF_UNIX,SOCK_STREAM,0); if(sockfd < 0) { perror("sock is err:"); return -1; } //2. 填充结构体 struct sockaddr_un saddr; saddr.sun_family = AF_UNIX; strcpy(saddr.sun_path,"./myunix"); if(connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) < 0) { perror("connect is err:"); return -1; } char buf[128] = ""; int recvbyte; while(1) { fgets(buf,sizeof(buf),stdin); if(buf[strlen(buf) -1] == '\n') buf[strlen(buf) -1] = '\0'; send(sockfd,buf,sizeof(buf),0); } close(sockfd); return 0; }