三、网络属性
一二章请点击:网络编程_1(网络基础+跨主机传输)
1.getsockop 和 setsockopt
功能:获取和设置网络属性;
头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
原型:
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数:
int sockfd:要设置/获取属性的套接字
int level:指定要控制套接字的层次;
1) SOL_SOCKET:通用套接字选项
2) IPPROTO_IP: ip选项
3) IPPROTO_TCP:TCP选项。
int optname:指定控制方式,看下图。
void *optval:获取或者设置套接字选项的变量地址,根据指定控制方式的数据类型进行转换。
socklen_t *optlen:第四个 void *optval指向的参数的大小;
返回值:
成功,返回0;
失败,返回-1, 更新errno;
例子
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, const char *argv[])
{
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0)
{
perror("socket");
return -1;
}
int flag = 0;
int len = sizeof(flag);
//获取接收缓冲区的大小
if(getsockopt(sfd, SOL_SOCKET, SO_RCVBUF, &flag, &len)<0)
{
perror("getsockopt");
return -1;
}
printf("%d\n", flag);
//发送缓冲区
if(getsockopt(sfd, SOL_SOCKET, SO_SNDBUF, &flag, &len) < 0)
{
perror("getsockopt");
return -1;
}
printf("%d\n", flag);
//是否允许快速重用本地端口 0:不允许, 1:允许
getsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &flag, &len);
printf("%d\n", flag);
//设置本地端口快速重用
int reuse = 1;
int r_len = sizeof(reuse);
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, r_len);
getsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &flag, &len);
printf("%d\n", flag);
//获取接收超时时间
struct timeval opttime;
int tlen = sizeof(opttime);c
getsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO, &opttime, &tlen);
printf("%lds%ldms\n", opttime.tv_sec, opttime.tv_usec);
return 0;
}
四、UDP
某一层一个协议可能对应多个不同协议,这样写代码很麻烦,换一个协议就得改很多那我们可使用一个机制socket,把协议族指定起来,在Linux种一切皆文件的设计理念种,网络也是文件,网络之间的通讯也可以向操作文件一样对他进行读写。
(一)、UDP模型
1、socket
用来获取网络操作的文件描述符,就像open函数一样
头文件:
#include <sys/types.h>
#include <sys/socket.h>
原型:
int socket(int domain, int type, int protocol);
参数:
int domain:协议族
AF_UNIX / AF_LOCAL 本地协议族 //在UNIX通信中用到
AF_INET IPv4协议族
AF_INET6 IPv6协议族
int type:套接字的类型
SOCK_STREAM:流式套接字,对应着TCP
特点有序,可靠、双工、基于连接的,以字节流位单位。
可靠不是指不丢包,是保证只要你能接收到这个包,
那么这个包的数据完整性一定正确的。
双工:同时收发。
字节流:数据没有明显的界限,一端数据可以分为任意多个包发送。
SOCK_DGRAM:报式套接字,对应着UDP
无连接,固定最大长度,不可靠消息。
就像写信,无法保证发出的信对方一定能收到,而且不能保证内容不会被篡改
不能保证哪封信先到,大家都能收到这个包,
但是发现不是自己的之后就丢弃。
发现是自己的包再处理。
由严格的数据分界
int protocol:
参数通常填0,默认协议。
返回值:
成功返回代表当前网络连接的文件描述符(套接字)
失败-1设置errno
2、bind(2)
功能:绑定ip和端口到套接字;
头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
int sockfd:socket创建的套接字;
struct sockaddr *addr:通用结构体。一般不采用这个结构体,不同的协议族采用不同的结构体,然后强转传入函数中。
存储要绑定到套接字上的IP和端口的地址结构体
需要采用IPV4的地址结构体,使用的时候强制转换。
man 7 ip
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
struct sockaddr_in {
//通用结构体,一般不写,为了不同格式的结构体强制转换成它传入函数
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */ 端口的网络字节序(1024~49151)
//如果是数字6666,则需要用函数htons()转化为网络字节序
//如果是命令行传参,则需要用htons(atoi(argv[1]))转换
struct in_addr sin_addr; /* internet address */ ip的网络字节序
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
socklen_t addrlen:地址结构体的大小,sizeof(struct sockaddr_in);
返回值:
成功,返回0;
失败,返回-1,更新errno;
3、recvfrom(2)
功能:接收客户端的数据,并获取客户端的ip和端口;
头文件:
#include <sys/types.h>
#include <sys/socket.h>
原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len,
int flags,
struct sockaddr *src_addr,
socklen_t *addrlen);
参数:
int sockfd:套接字;
void *buf:存储接收到的数据;
size_t len:要存储的长度;
int flags:指定要接收的方式,填0,阻塞方式接收;
struct sockaddr *src_addr:通用结构体,存储发送端的ip及端口号。如果不想接收,填NULL;
ipv4结构体
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */ 端口的网络字节序(1024~49151)
struct in_addr sin_addr; /* internet address */ ip的网络字节序
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
socklen_t *addrlen:地址结构体的大小。如果不想接收,可以填NULL;
返回值:
>0 返回接收的字节数;
=0 连接终止,只适用于TCP;
<0 错误,更新errNo;
4、sendto
功能:发送数据给客户端;
头文件:
#include <sys/types.h>
#include <sys/socket.h>
原型:
ssize_t sendto(int sockfd, const void *buf, size_t len,
int flags,
const struct sockaddr *dest_addr,
socklen_t addrlen);
参数:
int sockfd:套接字;
const void *buf:发送的数据;
size_t len:发送数据的大小;
int flags:指定发送方式,填0,阻塞方式发送。
如果没有数据发送,则将阻塞在该函数上.
struct sockaddr *dest_addr:存储目标的ip和端口号信息
socklen_t addrlen:地址结构体的大小,sizeof(struct sockaddr_in);
返回值:
成功,返回发送的字节数;
失败,返回-1,更新errno;
例子
服务器
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
if(argc < 3)
{
fprintf(stderr, "参数错误 ip port");
return -1;
}
int r_port = atoi(argv[2]);
if(r_port < 1024 || r_port > 49151)
{
fprintf(stderr, "非法端口号");
return -1;
}
printf("%d\n", r_port);
//1.创建套接字
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0)
{
perror("socket");
return -1;
}
printf("创建套接字成功 %d\n", sfd);
//2.绑定端口和ip
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(r_port); //端口:1024~49151
//sin.sin_addr.s_addr = inet_addr("0.0.0.0"); //泛指本机,也可以用INADDR_ANY,本质是0的宏
sin.sin_addr.s_addr = inet_addr(argv[1]); "192.168.1.111"
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
perror("bind");
close(sfd);
return -1;
}
printf("绑定成功\n");
char buf[200] = "";
//要接收客户端的IP和端口号,所以要定义一个客户端的结构体
struct sockaddr_in cin;
socklen_t size = sizeof(cin);
int port = 0;
char ip[20] = "";
while(1)
{
//接收
bzero(buf, sizeof(buf));
if(recvfrom(sfd, buf, 200, 0, \
(struct sockaddr*)&cin, &size)<=0)
{
perror("recvfrom");
return -1;
}
//端口号
port = 0;
port = ntohs(cin.sin_port); //把接收到的客户端的网络字节序的端口号转化为本地字节序端口号
//IP
bzero(ip, 20);
//把接收到的客户端的网络字节序的IP转化为本地字节序IP
inet_ntop(AF_INET, &cin.sin_addr, ip, 20);
printf("[%s:%d]:%s\n", ip, port, buf);
/*
bzero(buf, 200);
fgets(buf, 200-1, stdin);
//发送
if(sendto(sfd, buf, strlen(buf), 0,\
(struct sockaddr*)&cin, sizeof(cin)) < 0)
{
perror("sendto");
return -1;
}
printf("发送成功\n");c
*/
}
close(sfd);
return 0;
}
客户端
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/types.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//1.创建套接字
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if(cfd < 0)
{
perror("socket");
return -1;
}
//bind 非必须
//填充服务器的信息
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(2021); //服务器的端口号
sin.sin_addr.s_addr = inet_addr("192.168.1.109");
// sin.sin_addr.s_addr = inet_addr("0");
printf("客户端启动成功\n");
char buf[200] = "";
while(1)
{
//从终端获取
bzero(buf, 200);
fprintf(stderr, "请输入:");
fgets(buf, 200-1, stdin);
if(sendto(cfd, buf, strlen(buf), 0, \
(struct sockaddr*)&sin, sizeof(sin))<0)
{
perror("sento");
close(cfd);
return -1;
}
if(strncasecmp(buf, "quit", 4) == 0)
{
break;
}
/*
//接收
bzero(buf, sizeof(buf));
if(recvfrom(cfd, buf, 200, 0, NULL,NULL)<=0)
{
perror("recvfrom");
return -1;
}
printf("%s\n", buf);
*/
}
close(cfd);
return 0;
}
(二)多点通讯
1)广播
1.概念
1)如果同时给同一网段下的所有主机发送数据称之广播
2)只有UDP(报式套接字)才能广播
3)广播地址(主机号全是1)
例子:192.168.1.0那么他的广播地址是 192.168.1.255;
255.255.255.255 给所有网段中的所有主机发送广播
2.广播的发送流程(客户端)
1)创建报式套接字 (socket)
2)可选择绑定也可以不绑定(bind)
3)设置网络属性:允许广播,如果不设置,默认是不允许的(setsockopt)
4)接收端(服务器端)指定为广播地址、同时指定端口号 (填充服务器的信息)
5)发送数据 (sendto)
3.广播的接收流程(服务器)
1)创建报式套接字(socket)
2)绑定IP地址(广播IP地址(例如:192.168.1.255 或者 0.0.0.0(INADDR_ANY)))和端口号。(bind)
注意:绑定的端口号必须和客户端填充的端口号的一样;
3)等待接收数据(recvfrom)
例子
发送端(客户端)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <string.h>
#include <stdlib.h>
#define PORT 2020
#define IP "192.168.1.255"
int main(int argc, const char *argv[])
{
//1.socket
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd < 0)
{
perror("socket");
return -1;
}
//2.bind可选
//3.允许广播
int broadcast = 1;
if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, \
&broadcast, sizeof(int))<0)
{
perror("setsockopt");
return -1;
}
//4.填充服务器信息
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT); //该端口号是服务器的端口号
sin.sin_addr.s_addr = inet_addr(IP); //填充广播地址,主机号全为1
char buf[BUFSIZ] = "";
while(1)
{
//5.发送
bzero(buf, sizeof(buf));
fprintf(stderr, "请输入:");
fgets(buf, BUFSIZ-1, stdin);
if(sendto(fd, &buf, strlen(buf), 0, \
(struct sockaddr*)&sin, sizeof(sin))<0)
{
perror("sendto");
return -1;
}
if(strncasecmp(buf, "exit", 4) == 0)
{
break;
}
sleep(1);
}
close(fd);
return 0;
}
接收端(服务器端)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <string.h>
#include <stdlib.h>
#define PORT 2020
#define IP "192.168.1.255"
int main(int argc, const char *argv[])
{
//1.创建套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd < 0)
{
perror("socket");
return -1;
}
printf("创建套接字成功\n");
//允许快速重用本地端口
int reuse = 1;
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))<0)
{
perror("setsockopt");
return -1;
}
//绑定服务器的ip和端口
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.s_addr = inet_addr(IP);
//2.绑定
if(bind(fd, (struct sockaddr*)&sin, sizeof(sin))<0)
{
perror("bind");
exit(1);
}
//定义发送方的结构体
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
char buf[BUFSIZ] = "";
char ip[20] = "";
while(1)
{
//接收
bzero(buf, sizeof(buf));
if(recvfrom(fd, buf, BUFSIZ, 0,(struct sockaddr*)&cin, &len)<0)
{
perror("recvfrom");
exit(1);
}
//提取ip
bzero(ip, sizeof(ip));
inet_ntop(AF_INET, &cin.sin_addr, ip, 20);
printf("[%s:%d]:%s\n", ip, ntohs(cin.sin_port), buf); //注意要转换为本地字节序
if(strncasecmp(buf, "exit",4) == 0)
{
break;
}
}
close(fd);
return 0;
}
拓展:
//填充信息
struct msg_st sbuf;
memset(&sbuf,'\0',sizeof(sbuf));
strcpy(sbuf.name,"zhangsan");
srand(time(NULL));//设置随机数种子,
//这样每次执行的时候 rand产生的值都不一样。
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
//填充接收端地址信息
struct sockaddr_in raddr;
raddr.sin_family = AF_INET;
raddr.sin_port = htons(RCVPORT);
if( inet_pton(AF_INET,"255.255.255.255",&raddr.sin_addr.s_addr) !=1) {c
perror("转换失败");
exit(6);
}
2)组播
1. 概念
1)广播方式是发送给同一网段下的所有主机,过多的广播会占用大量网络带宽,会造成广播风暴,影响正常通讯;
2)组播(多播),加入到多播组中的主机才可以收发消息;
3)多播方式既可以给多个主机发送,又能避免造成过多负载。
4)每台主机要到传输层才能判断广播是否要处理
5)组播地址:D类 IP地址:224.0.0.0~239.255.255.255
2.组播的发送流程(客户端)
1)创建报式套接字(socket)
2)接收方(服务器)的地址指定为组播地址和端口号
3)发送数据
3. 组播的接收流程(服务器)
1)创建报式套接字(socket)
2)加入多播组
3)绑定组播IP(例如:224.1.2.3 或者 0.0.0.0)地址和端口
注意:绑定的端口号必须和发送方指定的端口号一致
4)等待接收数据;
4. 组播实现
组播是在IPPROTO_IP man 7 IP
功能:获取和设置网络属性;
头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
原型:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数:
int sockfd:要设置/获取属性的套接字
int level:指定要控制套接字的层次;
2) IPPROTO_IP: ip选项
int optname:指定控制方式,
IP_MULTICAST_IF: 创建多播组(这部可以省略)
IP_ADD_MEMBERSHIP: 加入多播组
IP_DROP_MEMBERSHIP: 脱离多播组
void *optval:获取或者设置套接字选项的变量地址,根据指定控制方式的数据类型进行转换。
struct ip_mreqn
{
struct in_addr imr_multiaddr; 多播组的ip,网络字节序 224.1.2.3-->网络字节序
struct in_addr imr_address; 本机IP地址,网络字节序 0.0.0.0(192.168.1.105)-->网络字节序
int imr_ifindex; 当前使用的网络设备索引号。
/*1.使用 $ ip ad命令查看网络设备索引号*/
/*2.通过函数的方式获取:*/
if_nametoindex(网卡的名字) //通过ifconfig获取网卡的名字
例子: imr_ifindex = if_nametoindex("eth0");
/*3.填0:默认索引号*/
};
socklen_t optlen:第四个 void *optval指向的参数的大小; sizeof(struct ip_mreqn)
返回值:
成功,返回0;
失败,返回-1, 更新errno;
例子
发送端(客户端)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#define PORT 2020
#define GROUP "224.1.2.3"
struct msg
{
int num;
char buf[128];
}__attribute((packed))__;
int main(int argc, const char *argv[])
{
//1)创建报式套接字(socket)
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd < 0)
{
perror("socket");
return -1;
}
//2)创建多播组(省)
//3)接收收方(服务器)的地址指定为组播地址和端口号
//填充服务器的ip和端口
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.s_addr = inet_addr(GROUP);
printf("填充完毕\n");
struct msg snd;
int num = 0;
snd.num = 0;
while(1)
{
bzero(snd.buf, sizeof(snd.buf));
//4)发送数据
fprintf(stderr, "请输入:");
fgets(snd.buf, sizeof(snd.buf), stdin);
//int类型转网络字节序
num++;
snd.num = htonl(num);
if(sendto(fd, &snd, sizeof(snd), 0, (struct sockaddr*)&sin,\
sizeof(sin))<0)
{
perror("sendto");
exit(1);
}
printf("发送成功\n");
}
close(fd);
return 0;
}
接收端(服务器)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#define PORT 2020
#define GROUP "224.1.2.3"
struct msg
{
int num;
char buf[128];
}__attribute((packed))__;
int main(int argc, const char *argv[])
{
//1)创建报式套接字(socket)
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd < 0)
{
perror("socket");
return -1;
}
//2)加入多播组
struct ip_mreqn mreq;
inet_pton(AF_INET, GROUP, &mreq.imr_multiaddr); //填充多播组的ip,网络字节序
inet_aton("0.0.0.0", &mreq.imr_address); //本机ip
mreq.imr_ifindex = 0; //网络设备索引号
// mreq.imr_ifindex = 2; //终端输入ip ad查找
// mreq.imr_ifindex = if_nametoindex("eth0"); //用函数获取设备所以号
if(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq))<0)
{
perror("setsockopt");
exit(1);
}
printf("加入多播组成功\n");
//允许本地端口快速重用
int reuse = 1;
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))<0)
{
perror("setsockopt");
exit(1);
}
//3)绑定组播IP(例:224.1.2.3 或者 0.0.0.0)地址和端口
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.s_addr = inet_addr(GROUP); //注意该位置填的是组播的ip或者0.0.0.0
// sin.sin_addr.s_addr = inet_addr("0.0.0.0");
if(bind(fd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(1);
}
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
struct msg rcv;
char ip[20] = "";
//4)等待接收数据;
while(1)
{
bzero(rcv.buf, sizeof(rcv.buf));
if(recvfrom(fd, &rcv, sizeof(rcv), 0, (struct sockaddr*)&cin, &len)<0)
{
perror("recvfrom");
exit(1);
}
//ip转换
bzero(ip, 20);
inet_ntop(AF_INET, &cin.sin_addr, ip ,20);
//端口转换
int port = ntohs(cin.sin_port);
//结构体中的num转换
int num = ntohl(rcv.num);
printf("[%s:%d]:num=%d,%s\n", ip, port, num, rcv.buf);
}
close(fd);
return 0;
}