目录
代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. UDP
1.1.UDP概念
1.传输层协议
2.面向无连接的, 不安全(会丢失数据)的, 报式(报文)传输协议
无连接:没有连接,通信不需要建立连接
不安全:数据会丢失
客户端给服务器发送1k数据, 服务器没收到
全部没收到->可能
收到了一部分->不可能
报文传输:发送和接收的数据是等量的
只有收到没收到
收到了就是全部的
没收到就是一丁点都没有收到
优势: 效率高
报式(UDP):发多少收多少--每次发送和接收的数据是等量的
一次发10k,对方就一次收10k
流式(TCP):发多少,不一定收多少--每次发送和接收的数据是不等量的
一次发10k,对方可以分十次接受 每次收1k
Qt中
// udp -> QUdpSocket
// Tcp -> QTcpServer (监听) QTcpSocket(通信)
1.2.UDP通信流程
1.2.1.UDP操作函数
// 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
- sockfd: 通信的文件描述符
- buf: 接收数据的缓冲区(数组地址)
- len: buf的容量
- flags: 写0, 使用默认属性
- src_addr: 发送数据的那一方地址信息: IP, Port, 地址族协议 -> 传出参数
- addrlen: 传入传出参数, src_addr 对应的内存大小
// 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
- sockfd: 通信的文件描述符
- buf: 存储了要发送的数据
- len: 要发送的数据的长度 -> 通过strlen() 计算
- flags: 写0, 使用默认属性
- dest_addr: 接收数据的一方的地址信息
- addrlen: 传入, dest_addr对应的内存大小
1.2.2.通信流程
// 服务器端
1. 创建用于通信的套接字 -> 使用报式协议( SOCK_DGRAM )
int cfd = socket(af_inet, SOCK_DGRAM, 0);
2. 绑定本地的IP和端口 和 用于通信的套接字绑定
bind();
3. 通信
- 接收数据: recvfrom
- 发送数据: sendto
4. 关闭连接: close(cfd);
// 客户端通信流程:
1. 创建用于通信的套接字 -> 使用报式协议( SOCK_DGRAM )
int cfd = socket(af_inet, SOCK_DGRAM, 0);
2. 通信
- 接收数据: recvfrom
- 发送数据: sendto
3. 关闭连接: close(cfd);
client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
int main()
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == fd)
{
perror("socket");
exit(0);
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
unsigned int lenser = sizeof(serverAddr);
inet_pton(AF_INET, "192.168.184.134", &serverAddr.sin_addr.s_addr);
int num = 0;
while (1)
{
char buf[1024] = {0};
sprintf(buf, "----你好服务器--%d...", num++);
sendto(fd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&serverAddr, lenser);
recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("服务器发送来的数据是:%s\n", buf);
sleep(1);
}
close(fd);
return 0;
}
server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
int main()
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == fd)
{
perror("socket");
exit(0);
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverAddr.sin_addr.s_addr);
int ret = bind(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (-1 == ret)
{
perror("bind");
exit(0);
}
while (1)
{
char buf[1024] = {0};
struct sockaddr_in clientAddr;
unsigned int lencli = sizeof(clientAddr);
recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&clientAddr, (socklen_t *)&lencli);
char ip[17] = {0};
inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, ip, sizeof(ip));
unsigned short int port = ntohs(clientAddr.sin_port);
printf("client IP: %s, Port: %d, Data: %s\n", ip, port, buf);
sprintf(buf, "客户端的IP:%s,Port:%d", ip, port);
sendto(fd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&clientAddr, lencli);
}
close(fd);
return 0;
}
2. 广播
概念
向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
- 只能在局域网中使用
- 客户端只要绑定了服务器广播使用的端口, 就可以接收到广播数据
UDP天然支持广播
UDP广播:通信流程
// 广播的一端
1. 创建用于通信的套接字 -> 使用报式协议( SOCK_DGRAM )
int cfd = socket(af_inet, SOCK_DGRAM, 0);
2. 设置广播属性
setsockopt();
3. 通信之前的准备工作--设置广播地址和广播端口:发送数据时使用
struct sockaddr_in recvAddr;
recvAddr.sin_family = af_inet;
//接收数据的一方需要绑定这个端口
recvAddr.sin_port = htons(8989);
// 需要指定广播地址
inet_pton(af_inet, "xx.xx.xx.255", &recvAddr.sin_addr.s_addr);
4. 发送广播消息
sendto(cfd, msg, msgLen, 0, &recvAddr, sizeof(recvAddr));
5. 关闭连接: close(cfd);
// 接收广播的一端
1. 创建用于通信的套接字 -> 使用报式协议( SOCK_DGRAM )
int cfd = socket(af_inet, SOCK_DGRAM, 0);
2. 端口绑定 -> 需要和发送广播一端发送数据指定的端口对应
struct sockaddr_in addr;
//接受广播数据的端口
addr.sin_port = htons(8989);
bind(cfd, &addr, sizeof(addr));
3. 通信
- 接收数据: recvfrom
4. 关闭连接: close(cfd);
设置广播属性的函数
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
- sockfd: udp通信的套接字
- level: SOL_SOCKET
- optname: SO_BROADCAST
- optval: 整形数。 值为1:设置广播;值为0:不设置广播属性
- optlen: optval参数对应的内存大小 -> sizeof(optval)
广播的特点:
1. 在局域网范围内只要是客户端绑定的对应的端口, 都可以收到广播消息
- 在这种情况下, 是不能拒绝接收广播消息的
2. 只能在局域网中使用, 不能在广域网中使用
3. 必须要使用广播地址 -> xxx.xxx.xxx.255
client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
int main()
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == fd)
{
perror("socket");
exit(0);
}
struct sockaddr_in Addr;
Addr.sin_family = AF_INET;
Addr.sin_port = htons(8888); // 监听的port和广播的port相同
//inet_pton(AF_INET, "192.168.184.134", &Addr.sin_addr.s_addr);
Addr.sin_addr.s_addr = INADDR_ANY; //自动绑定本地IP
int ret = bind(fd, (struct sockaddr *)&Addr, sizeof(Addr));
if (-1 == ret)
{
perror("bind");
exit(0);
}
while (1)
{
char buf[1024] = {0};
recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("服务端发来的数据是: %s\n", buf);
}
close(fd);
return 0;
}
server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
int main()
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == fd)
{
perror("socket");
exit(0);
}
//设置广播属性
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
struct sockaddr_in Addr;
Addr.sin_family = AF_INET;
Addr.sin_port = htons(8888); //广播的port
inet_pton(AF_INET, "192.168.184.255", &Addr.sin_addr.s_addr); //广播的ip
int num = 0;
while (1)
{
char buf[1024] = {0};
sprintf(buf, "你好客户端:%d", num++);
sendto(fd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&Addr, sizeof(Addr));
sleep(1);
}
close(fd);
return 0;
}
3. 组播/组播
概念&特点
广播时Server给局域网的交换机发送数据,无论连接到局域网的客户端想不想接收该数据,Server都会给客户端发送该数据。—>进而造成客户端上数据的拥塞—>因此引出了多播(组播)
特点:
- Server可以将数据包只发送给指定组内的客户端,而不发送给指定组外的客户端。
- 可以在internet(广域网)中进行组播,可以在局域网中进行组播
- 加入到组播地址中才能接收到数据
- 组播也有特定的组播地址, 需要人为指定才可以
组播地址
IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从
224.0.0.0
到239.255.255.255
,并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类:
IP地址 | 说明 |
---|---|
224.0.0.0~224.0.0.255 | 局部链接多播地址:是为路由协议和其它用途保留的地址,路由器并不转发属于此范围的IP包 |
224.0.1.0~224.0.1.255 | 预留多播地址:公用组播地址,可用于Internet;使用前需要申请 |
224.0.2.0~238.255.255.255 | 预留多播地址:用户可用组播地址(临时组地址),全网范围内有效 |
239.0.0.0~239.255.255.255 | 本地管理组播地址,可供组织内部使用,类似于私有 IP 地址,不能用于 Internet,可限制多播范围 |
UDP组播:通信流程
// 发送组播的一端
1. 创建用于通信的套接字 -> 使用报式协议( SOCK_DGRAM )
int cfd = socket(af_inet, SOCK_DGRAM, 0);
2. 设置组播属性
setsockopt();
3. 通信之前的准备工作--设置组播地址和组播端口:发送数据时使用
struct sockaddr_in recvAddr;
recvAddr.sin_family = af_inet;
//接收数据的一方需要绑定这个端口
recvAddr.sin_port = htons(8989);
//需要指定组播地址
inet_pton(af_inet, "239.0.1.10", &recvAddr.sin_addr.s_addr);
4. 发送组播消息
sendto(cfd, msg, msgLen, 0, &recvAddr, sizeof(recvAddr));
5. 关闭连接: close(cfd);
// 接收组播的一端
1. 创建用于通信的套接字 -> 使用报式协议( SOCK_DGRAM )
int cfd = socket(af_inet, SOCK_DGRAM, 0);
2. 端口绑定 -> 需要和发送广播一端发送数据指定的端口对应
struct sockaddr_in addr;
// 接受组播数据的端口
addr.sin_port = htons(8989);
bind(cfd, &addr, sizeof(addr));
3. 加入到组播地址(加入到多播组)
setsockopt();
4. 通信
- 接收数据: recvfrom
5. 关闭连接: close(cfd);
UDP设置组播属性
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
- 设置组播属性:发送组播消息--那一端
- sockfd: 通信的fd
- level: IPPROTO_IP
- optname: IP_MULTICAST_IP
- optval: struct in_addr 类型->ip地址->组播的IP地址
- optlen: optval的内存大小, sizeof(optval)
- 加入到多播组:接收组播消息--那一端
- sockfd: 通信的fd
- level: IPPROTO_IP
- optname: IP_ADD_MEMBERSHIP
- optval: struct ip_mreqn 类型
- optlen: optval的内存大小, sizeof(optval)
typedef unsigned int uint32_t;
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
struct ip_mreqn // 美人去哪儿
{
struct in_addr imr_multiaddr;// 组播组的IP地址.
struct in_addr imr_address;// 本地某一网络设备接口的IP地址。
int imr_ifindex;//网卡编号
};
// 通过网卡名字得到网卡编号
#include <net/if.h>
unsigned int if_nametoindex(const char *ifname);
03_multicast_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <net/if.h>
int main()
{
//1.创建通信的套接字
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd == -1)
{
perror("socket");
exit(0);
}
//2.初始化本地地址信息:主要是绑定Port(接受数据使用)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9898);
addr.sin_addr.s_addr = INADDR_ANY;
bind(fd,(struct sockaddr*)&addr,sizeof(addr));
//3.加入到多播组
struct ip_mreqn mymreqn;
mymreqn.imr_address.s_addr = INADDR_ANY;//本地IP
inet_pton(AF_INET,"239.0.1.10",&mymreqn.imr_multiaddr.s_addr);//组播地址
mymreqn.imr_ifindex = if_nametoindex("ens33");//获取网卡信息
setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mymreqn,sizeof(mymreqn));
//4.通信:接收组播消息
int num = 0;
while(1)
{
char buf[1024];
recvfrom(fd,buf,sizeof(buf),0,NULL,NULL);
printf("服务器的数据:%s\n",buf);
}
//5.关闭fd
close(fd);
return 0;
}
03_multicast_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
//1.创建通信的套接字
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd == -1)
{
perror("socket");
exit(0);
}
//2.设置组播属性
struct in_addr myaddr;
inet_pton(AF_INET,"192.168.184.132",&myaddr.s_addr);
setsockopt(fd,IPPROTO_IP,IP_MULTICAST_IF,&myaddr,sizeof(myaddr));
//3.初始化组播的地址信息:发送时使用(组播端口和组播地址)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9898);
inet_pton(AF_INET,"239.0.1.10",&addr.sin_addr.s_addr);
//4.通信:组播发送数据
int num = 0;
while(1)
{
char buf[1024];
sprintf(buf,"你好客户端...%d...",num++);
sendto(fd,buf,strlen(buf)+1,0,(struct sockaddr*)&addr,sizeof(addr));
sleep(1);
}
//5.关闭fd
close(fd);
return 0;
}
4. 本地套接字
1.本地套接字: 只能用于某个终端上的进程之间的通信
- 有血缘关系
- 没有血缘关系
2.通信流程
- 按照tcp的通信流程编写:常用这种方式
- 按照udp的通信流程编写
结构体
// 头文件: sys/un.h
#include <sys/un.h>
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; // 地址族协议 af_local
char sun_path[UNIX_PATH_MAX];// 套接字文件的路径, 这是一个伪文件, 大小永远=0
};
通信流程
// 服务器端进程
1. 创建套接字, 得到一个文件描述符
int fd = socket(int domain, int type, int protocol);
- 参数1: AF_UNIX, AF_LOCAL
- 参数2:
- SOCK_STREAM: 按照tcp流程通信
- SOCK_DGRAM: 按照udp流程通信
- 参数3: 0
int fd = socket(AF_LOCAL,SOCK_STREAM,0)
2. 将第一步得到的文件描述符和本地的IP端口进行绑定
struct socaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock");
// 绑定成功 套接字文件会被创建: server.sock
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
3. 设置监听(绑定成功的文件描述符) -> 成功之后可以接收客户端连接
listen();
4. 如果有客户端连接服务器, 服务器接受请求并建立连接, 得到一个文件描述符
struct sockaddr_un cliaddr;
int fd1 = accept(fd, (struct sockaddr*)cliaddr, &len);
5. 通信
- 接收数据
read();
recv();
- 发送数据
write();
send();
6. 通信完成, 关闭文件描述符
close();
// 客户端进程
1. 创建套接字
int fd = socket(int domain, int type, int protocol);
- 参数1: AF_UNIX, AF_LOCAL
- 参数2:
- SOCK_STREAM: 按照tcp流程通信
- SOCK_DGRAM: 按照udp流程通信
- 参数3: 0
2. 客户端也需要绑定一个本地套接字
struct socaddr_un addr;
addr.sun_family = af_unix;
strcpy(addr.sun_path, "client.sock"); // 客户端绑定的套接字文件
// 绑定成功 套接字文件会被创建: client.sock
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
3. // 客户端的端口不需要调用函数绑定, 自动就绑定了, 如果非得自己绑定也可以 -> bind
连接服务器: 在客户端需要知道服务器绑定的IP和端口是多少
struct sockaddr_un seraddr;
seraddr.sun_family = af_local;
strcpy(seraddr.sun_path, "server.sock"); // 指定服务器端使用的套接字文件
connect(fd, (struct sockaddr*)&seraddr, sizeof(seraddr));
4. 通信
- 接收数据
read();
recv();
- 发送数据
write();
send();
5. 断开连接
close():
04_localipc_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/un.h>
int main()
{
// 1. 创建用于通信的套接字
int fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定一个本地套接字
struct sockaddr_un cliaddr;
cliaddr.sun_family = AF_LOCAL;
strcpy(cliaddr.sun_path, "client.sock"); // 客户端绑定的套接字文件
bind(fd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
// 3. 连接服务器
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock"); // 服务器端绑定的套接字文件
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("connect");
exit(0);
}
int i = 0;
// 通信
while(1)
{
// 读数据
char recvBuf[1024];
// 写数据
sprintf(recvBuf, "data: %d\n", i++);
write(fd, recvBuf, strlen(recvBuf)+1);
// 如果客户端没有发送数据, 默认阻塞
read(fd, recvBuf, sizeof(recvBuf));
printf("recv buf: %s\n", recvBuf);
sleep(1);
}
// 释放资源
close(fd);
return 0;
}
04_localipc_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/un.h>
int main()
{
// 1. 创建用于监听的套接字
int fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定本地
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock");
// 绑定成功server.sock文件被创建
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3.设置监听
ret = listen(fd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 等待, 接受连接请求
struct sockaddr_un addrCli;//存储客户端的addr
int len = sizeof(addrCli);
printf("正在焦急等待客户端的连接...\n");
int connfd = accept(fd, (struct sockaddr*)&addrCli, &len);
if(connfd == -1)
{
perror("accept");
exit(0);
}
// 得到客户端进程绑定的本地套接字文件的名字
printf("client socket fileName: %s\n", addrCli.sun_path);
int num = 0;
// 通信
while(1)
{
// 读数据
char recvBuf[1024];
// 如果客户端没有发送数据, 默认阻塞
int ret = read(connfd, recvBuf, sizeof(recvBuf));
if(ret == -1)
{
perror("read");
break;
}
else if(ret == 0)
{
printf("客户端已经断开了连接...\n");
break;
}
else
{
printf("recv buf: %s\n", recvBuf);
// 写数据
write(connfd, recvBuf, strlen(recvBuf));
}
}
// 释放资源
close(connfd); // 通信
close(fd); // 监听
return 0;
}