十三、网络IPC 套接字(16章)
网络IPC 套接字
#协议族 man 7 中章节有介绍
$ cd /proc/
$ netstat -anu //查看udp 报式传输
$ netstat -anu | grep xxxx //查看udp 报式传输
$ netstat -ant //流式套接字
13.1 网络编程相关
进程标识:计算机的网络地址,可以帮助我们标识网络上我们想与之通信的计算机;另一部分是该计算机上用端口号表示的服务,可以帮助我们标识特定的进程。
跨主机之间通信需要注意的地方
1、数据类型长度:
int32_t uint32_t
int8_t uint8_t
int16_t int64_t
2、字节序:大端 小端
什么是大端Big-Endian 和小端Little-Endian ?
大端模式:就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
小端模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
3、结构体对齐 – 解决不对齐
__attribute__((packed));
//UDP数据包 结构体一定不能考虑对齐,因为每个平台的上面的对齐方式可能会有差异,所以告诉gcc,该结构体 不需要对齐。
4、不能使用指针类型进行传输
注意,只要涉及到网络传输内容,传输的内容从来不会用到指针,因为如果是跨主机传输,两个设备上的某个地址中的数据是不同的,传递指针没有意义
网络7层协议(7层是指OSI七层协议模型)
主要是:应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。
13.2 套接字描述符
字节序 htonl、ntohl…(寻址)
socket()
函数功能:创建一个用于数据交换的套接字
套接字是全双工的
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/* domain */
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK AppleTalk ddp(7)
AF_PACKET Low level packet interface packet(7)
AF_ALG Interface to kernel crypto API
参数:
- domain:域,如协议族
man 7 ip
- type:确定套接字的类型
- protocol:协议族中的哪个协议,0 代表 IPPROTO_UDP
返回值:文件描述符
int type | 特点 |
---|---|
SOCK_STREAM | 流式、有序、可靠、双工、基于连接 |
SOCK_DGRAM | 默认报式 |
SOCK_SEQPACKET | 有序、可靠、数据有严格 |
SOCK_RDM | |
SOCK_RAW |
流式套接字的特点:通信前需要创建连接,确认应答机制,通信结束要正常断开连接.
报式套接字的特点:不需要创建连接,可能会出现数据丢失,数据传输效率高,数据以数据包形式表达传输.
主动端也就是先发包的那一方可以省略取得地址,系统会默认分配
bind() 是给SOCKET取得地址,即绑定本地地址。这个操作是和本机的约定。
发送端如果不和本机约定地址,即省略bind()操作。当前SOCKET建立成功后。系统会为我们分配一个可用的空闲的地址给我们用,在进程结束之前 该端口一直给我们用。
inet_pton()
函数功能:把点分式转换成大整数,即将点分式 转换为 二进制格式,并且自动会识别字节序
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数:
-
af:协议族,只能是 AF_INET or AF_INET6,即IPV4或者IPV6
-
src:待转换的ip地址
-
dst:可写的一块地址,转换之后的存储空间
inet_ntop()
函数功能:把大整数转换成点分式,即将网络字节序的二进制地址转换成文本字符串格式。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
参数:
-
af:协议族,只能是 AF_INET or AF_INET6,即IPV4或者IPV6
-
src:待转换的二进制形式的地址
-
dst:可写的一块地址,转换之后的存储空间
bind()
函数功能:绑定SOCKET到本地地址(接收端一定需要)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
}
参数:
- sockfd:获取 SOCKET 返回的文件描述符
- addr:协议地址,依赖于当前用到的协议族中的地址信息,AF_INET 协议族中的 协议地址类型为 struct sockaddr_in
- addrlen:协议地址空间大小
同的协议族 来 绑定自己这端的地址 所用的结构体是不一样的。 所以是不存在 struct sockaddr 类型的。所以我们的处理方式是:我们用的是哪一个协议族,就把该协议族地址作为addr ,然后再把地址长度写到addrlen
AF_INET see ip(7)-------man 7 ip
注意 :IP地址和端口,是需跟着网络一起发送的。代表自己的身份
struct sockaddr_in
{
sa_family_t sin_family; /* 协议族 address family: AF_INET */
in_port_t sin_port; /* 需要的端口 port in network byte order */
//IP地址 并非点分式,而是大整数internet address ,用的时候需要格式转换:inet_pton()
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};
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);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
参数:
- sockfd:目标SOCKET
- buf:发送数据信息存储地址
- len:发送数据信息存储空间 大小
- flags:特殊要求
- dest_addr:接收端的地址,即对端地址
- addrlen:接收端地址空间大小
返回值:成功发送的字节数 ,失败返回-1并设置errno
recvfrom()
函数功能:从 SOCKET上接收信息
#include <sys/types.h>
#include <sys/socket.h>
//应用于流式套接字,只需要指定目标SOCKET,和存储信息的位置即大小,以及有无特殊要求即可。因为是提前建立好链接的,一对一,点对点的连接方式,所以不用记录 对端是谁。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//用于报式套接字通信,接收的每一个消息来源可能不一致,所以除了需要指定目标SOCKET,和存储信息的位置即大小,以及有无特殊要求之外。还需要记录对方是谁,即发送端身份。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数:
- sockfd:目标SOCKET
- buf:接收信息存储地址
- len:接收信息存储空间 大小
- flags:特殊要求
- src_addr:用于保存发送端的地址,即保存对端地址,接收到消息的时候保存
- addrlen:发送端地址空间大小,即对端地址代表空间的大小
返回值:
成功收到字节的个数
只要是从网络上接到的数据,还不是单字节,就要ntoh
常见UDP报头 8
13.3 代码应用:报式套接字传输数据(UDP)
协议族:AF_INET
套接字类型:SOCK_DGRAM
报式套接字的特点:不需要创建连接,可能会出现数据丢失,数据传输效率高,数据以数据包形式表达传输.
Cilent(主动端)
1.socket():创建一个用于数据交换的套接字
2.bind():绑定SOCKET到本地地址 (可省略,系统会自动分配)
3.sendto():用于报式套接字 发送数据
Server(被动端)
1.socket:创建一个用于数据交换的套接字
2.bind():绑定SOCKET到本地地址(不可省略)
3.recvfrom:从 SOCKET上接收信息
Proto.h(协议族)
/* proto.h “apue/ipc/socket/dgram/basic” */
#ifndef PROTO_H__
#define PROTO_H__
#include <stdint.h>
#define RCVER_PORT "2989" //端口
#define NAMESIZE 13
//0~1024周知端口 1025~65535
struct msg_st
{
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed)); //单字节对齐
/*
UDP数据包 结构体一定不能考虑对齐,因为每个平台的上面的对齐方式可能会有差异,所以告诉gcc,该结构体 不需要对齐。
*/
/*
int是带符号的,表示范围是:-2147483648到2147483648,即-2^31到2^31次方。
uint则是不带符号的,表示范围是:2^32即0到4294967295。
uint可以使用十进制,二进制,十六进制。
*/
#endif
Cilent(主动端-发送端)
/* snder.c 发送端 */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include "proto.h"
#define IPSTRSIZE 128
int main(int argc,char *argv[])
{
int sd;
struct sockaddr_in raddr;
struct msg_st sbuf;
if(argc < 2)
{
fprintf(stderr,"Usage...\n");
exit(1);
}
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//bind(); 可省略绑定到本端地址,系统会自动分配
memset(&sbuf,'\0' ,sizeof(sbuf)); //将sbuf结构体里全部写入‘\0’
strcpy(sbuf.name,"Xiaolei");
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(RCVER_PORT));
inet_pton(AF_INET,argv[1],&raddr.sin_addr);
if(sendto(sd,&sbuf,sizeof(sbuf),0,(void *)&raddr,sizeof(raddr)) < 0)
{
perror("sendto()");
exit(1);
}
puts("OK");
close(sd);
exit(0);
}
/*
xiaolei@xiaolei:~/teacher/example/apue/ipc/socket/dgram/basic$ ./server
-----MESSAGE FROM:192.168.5.133:50556-------
NAME:Xiaolei
MATH:83
CHINESE:86
xiaolei@xiaolei:~/teacher/example/apue/ipc/socket/dgram/basic$ ./client 192.168.5.133
OK
*/
Server(被动端-接收端)
//rcver.c 接收端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 128
int main()
{
int sd;
struct sockaddr_in laddr,raddr;
struct msg_st rbuf;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
//取得SOCKET, 用 AF_INET协议族中 默认支持报式套接字的协议 来完成 报式SOCK_DGRAM套接字传输,
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//设置 AF_INET 协议族地址信息结构体,AF_INET 协议族中的 协议地址类型为 struct sockaddr_in
// 协议族为 AF_INET
laddr.sin_family = AF_INET;
// 设置端口为2989,因为需要将自己的地址信息(包括端口信息)发出去,所以需要注意字节序问题,即 从主机发向网络,htons
laddr.sin_port = htons(atoi(RCVER_PORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr); //把点分式转换成大整数
// 给SOCKET绑定一个地址,关联到目标协议族地址信息结构体 man 7 ip
if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
/* !!!! */
raddr_len = sizeof(raddr); //一定要注意初始化对端地址空间大小信息
while(1)
{
/*以报式套接字方式 从 SOCKET上接收信息,raddr用于保存发送端的地址,
即保存对端地址,接收到消息的时候保存,remote端地址信息。接收到的信息存储到rbuf*/
if(recvfrom(sd,&rbuf,sizeof(rbuf),0,(void *)&raddr,&raddr_len) < 0)
{
perror("recvfrom()");
exit(1);
}
/*转换IPv4 and IPv6 addresses 地址格式, 大整数格式 --> 点分式格式 存储到ipstr*/
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
/*raddr.sin_port) 是从socket接收到的信息,不是单字节信息,需要字节序转换 ntohs*/
printf("-----MESSAGE FROM:%s:%d-------\n",ipstr,ntohs(raddr.sin_port));
printf("NAME:%s\n",rbuf.name); //单字节信息,不需要字节序转换
printf("MATH:%d\n",ntohl(rbuf.math)); //需要字节序转换
printf("CHINESE:%d\n",ntohl(rbuf.chinese)); //需要字节序转换
}
close(sd);
exit(0);
}
/*
xiaolei@xiaolei:~/teacher/example/apue/ipc/socket/dgram/basic$ ./server
-----MESSAGE FROM:192.168.5.133:50556-------
NAME:Xiaolei
MATH:83
CHINESE:86
xiaolei@xiaolei:~/teacher/example/apue/ipc/socket/dgram/basic$ ./client 192.168.5.133
OK
*/
13.4 代码应用:动态报式套接字传输数据
/* proto.h*/
#ifndef PROTO_H__
#define PROTO_H__
#include <stdint.h>
#define RCVER_PORT "2989"
#define MSGMAX (512-8)
#define NAMEMAX (MSGMAX-8)
struct msg_st
{
uint32_t math;
uint32_t chinese;
uint8_t name[1]; //变长结构体思想
}__attribute__((packed));
//UDP数据包 结构体一定不能考虑对齐,因为每个平台的上面的对齐方式可能会有差异,所以告诉gcc,该结构体 不需要对齐。
#endif
/* rcver.c 接收端 */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 128
int main()
{
int sd;
struct sockaddr_in laddr,raddr;
struct msg_st *rbuf;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
//根据服务端 即将接受数据大小 申请空间 用于存储接收数据
rbuf = malloc(MSGMAX); //sizeof(struct msg_st) + NAMEMAX + 1;
if(rbufp == NULL)
{
perror("malloc()");
exit(1);
}
//创建Socket, 即确定传输协议族,传输协议,传输方式
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//设置 本端,即服务端 地址信息, struct sockaddr_in结构体信息:协议族类型,端口,IP
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(RCVER_PORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
//绑定 Socket 到 本端地址
if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
raddr_len = sizeof(raddr);
while(1)
{
//接收数据,数据存储于rbufp,将对端地址信息(struct sockaddr_in)存储于raddr
if(recvfrom(sd,rbuf,MSGMAX,0,(void *)&raddr,&raddr_len) < 0) //注意这里rbuf
{
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("-----MESSAGE FROM:%s:%d-------\n",ipstr,ntohs(raddr.sin_port));
printf("NAME:%s\n",rbuf->name);
printf("MATH:%d\n",ntohl(rbuf->math));
printf("CHINESE:%d\n",ntohl(rbuf->chinese));
}
close(sd);
free(rbuf);
exit(0);
}
/* snder.c 发送端 */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include "proto.h"
#define IPSTRSIZE 128
int main(int argc,char *argv[])
{
int sd,size;
struct sockaddr_in raddr;
struct msg_st *sbuf;
if(argc < 3)
{
fprintf(stderr,"Usage...\n");
exit(1);
}
//判断发送端传输 名字的大小是否过大
if(strlen(argv[2]) > NAMEMAX-1)
{
fprintf(stderr,"Name is too long.\n");
exit(1);
}
//需要发送的数据包大小,即柔型数组大小
size = sizeof(struct msg_st) + strlen(argv[2]);
sbuf = malloc(size);
{
perror("malloc()");
exit(1);
}
//创建Socket, 即确定传输协议族,传输协议,传输方式
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//bind(); 可省略绑定到本端地址,系统会自动分配
//拷贝命令行中的 名字到 数据包对应位置
strcpy(sbuf->name,argv[2]);
sbuf->math = htonl(rand()%100);
sbuf->chinese = htonl(rand()%100);
//设置 服务端地址信息, struct sockaddr_in结构体信息:协议族类型,端口,IP
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(RCVER_PORT));
inet_pton(AF_INET,argv[1],&raddr.sin_addr);
//向 服务端 发送数据
if(sendto(sd,sbuf,size,0,(void *)&raddr,sizeof(raddr)) < 0)
{
perror("sendto()");
exit(1);
}
puts("OK");
close(sd);
free(sbuf);
exit(0);
}
/* 实验结果
xiaolei@xiaolei:~/apue/ipc/socket/dgram/var$ ./snder 192.168.5.133 xiaolei
OK
xiaolei@xiaolei:~/apue/ipc/socket/dgram/var$ ./rcver
-----MESSAGE FROM:192.168.5.133:56892-------
NAME:
MATH:83
CHINESE:0
*/
13.5 多点通信-广播与组播
广播–全网广播 广播地址:255 255 255 255
组播–多播 多播组 组播地址【d类地址:224开头】:224.2.2.2
$ ip ad sh #查看索引号
$ man 7 socket
$ tcp ip socket
getsockopt() && setsockopt()
函数功能:获取设置socket选项
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
/*对某一个指定的Socket的某个层面上的众多属性中的某一个属性进行设置,不同属性参数可能类型不同,所以需要指明参数类型和大小
如:对指定Socket sd 的SOL_SOCKET层面 的 SO_BROADCAST属性进行设置,即打开该属性,表示允许将数据发送至广播地址,即打开广播属性。
int val = 1;
setsockopt(sd, SOL_SOCKET, SO_BROADCAST,&val, sizeof(val));
*/
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
//返回值:成功:0 失败: -1
参数:
-
sockfd:文件描述符
-
level:协议层次 如:SOL_SOCKET “套接字层次”
-
optname:选项的名称(套接字层次)
man 7 socket
-
optval:获取到的选项的值
-
optlen:value的长度
返回值:成功: 0 失败: -1
level | 协议层次 |
---|---|
SOL_SOCKET | 套接字层次 |
IPPROTO_TCP | TCP层次 |
IPPROTO_IP | ip层次 |
int optname | 选项的名称(套接字层次) |
---|---|
SO_BROADCAST | 是否允许发送广播信息 |
SO_REUSEADDR | 是否允许重复使用本地地址 |
SO_SNDBUF | 获取发送缓冲区长度 |
SO_RCVBUF | 获取接收缓冲区长度 |
SO_RCVTIMEO | 获取接收超时时间 |
SO_SNDTIMEO | 获取发送超时时间 |
广播篇:
广播是指将报文发送到网络中的所有可能的接收者。从原理上这很容易实现:路由器简单地将它接收到的任何广播报文副本转发到除该报文到达的接口以外的每个接口。当多台主机连接到同一个局域网时,广播还能结合链路层特点提供相对更高效的转发策略。
man 7 socket
层名:SOL_SOCKET
“代码演示:全网广播
注意点:想要针对广播 进行数据发送,接收 就必须要打开SO_BROADCAST属性,对于从广播接收数据,如果不打开广播属性,有可能收的到,也有可能收不到,如果想一定收到,就必须打开SO_BROADCAST属性
/**** proto.h ****/
#ifndef PROTO_H__
#define PROTO_H__
#include <stdint.h>
#define RCVER_PORT "2989"
#define NAMESIZE 13
struct msg_st
{
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed));
//UDP数据包 结构体一定不能考虑对齐,因为每个平台的上面的对齐方式可能会有差异,所以告诉gcc,该结构体 不需要对齐。
#endif
/* rcver.c 打开全网广播属性 */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include "proto.h"
#define IPSTRSIZE 128
int main()
{
int sd;
struct sockaddr_in laddr,raddr;
struct msg_st rbuf;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
//创建Socket, 即确定传输协议族,传输协议,传输方式
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//设置 本端,即服务端 地址信息, struct sockaddr_in结构体信息:协议族类型,端口,IP
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(RCVER_PORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
//绑定 Socket 到 本端地址
if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
/* !!!! */
raddr_len = sizeof(raddr);
while(1)
{
//接受数据,数据存储于rbufp,将对端地址信息(struct sockaddr_in)存储于raddr
if(recvfrom(sd,&rbuf,sizeof(rbuf),0,(void *)&raddr,&raddr_len) < 0)
{
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("-----MESSAGE FROM:%s:%d-------\n",ipstr,ntohs(raddr.sin_port));
printf("NAME:%s\n",rbuf.name);
printf("MATH:%d\n",ntohl(rbuf.math));
printf("CHINESE:%d\n",ntohl(rbuf.chinese));
}
close(sd);
exit(0);
}
/* snder.c 打开全网广播属性 */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include "proto.h"
#define IPSTRSIZE 128
int main(int argc,char *argv[])
{
int sd;
struct sockaddr_in raddr;
struct msg_st sbuf;
//创建Socket, 即确定传输协议族,传输协议,传输方式
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//bind(); 可省略绑定到本端地址,系统会自动分配
/*对指定Socket sd 的SOL_SOCKET层面 的 SO_BROADCAST属性进行设置,即打开该属性,表示允许将数据发送至广播地址,即打开广播属性。*/
int val = 1;
if(setsockopt(sd,SOL_SOCKET,SO_BROADCAST,&val,sizeof(val)) < 0)
{
perror("setsockopt()");
exit(1);
}
// 拷贝命令行中的 名字到 数据包对应位置
memset(&sbuf,'\0' ,sizeof(sbuf));
strcpy(sbuf.name,"Alan");
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
//设置 服务端地址信息, struct sockaddr_in结构体信息:协议族类型,端口,IP
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(RCVER_PORT));
//地址为广播地址,255.255.255.255 广播地址
inet_pton(AF_INET,"255.255.255.255",&raddr.sin_addr);
//向 服务端 发送数据
if(sendto(sd,&sbuf,sizeof(sbuf),0,(void *)&raddr,sizeof(raddr)) < 0)
{
perror("sendto()");
exit(1);
}
puts("OK");
close(sd);
exit(0);
}
/* 代码演示
xiaolei@xiaolei:~/apue/ipc/socket/dgram/bcast$ ./snder
OK
xiaolei@xiaolei:~/apue/ipc/socket/dgram/bcast$ ./rcver
-----MESSAGE FROM:192.168.5.133:41266-------
NAME:Alan
MATH:83
CHINESE:86
*/
组播篇:
man 7 ip
层名:IPPROTO_IP
IP_ADD_MEMBERSHIP (since Linux 1.2)
Join a multicast group. Argument is an ip_mreqn structure. //加入多播组
struct ip_mreqn
{
/* 多播组地址 大整数 IP multicast group address */
struct in_addr imr_multiaddr;
/* 当前自己IP地址 IP address of local interface */
struct in_addr imr_address;
/*网络索引号(读取命令: ip ad sh) interface index */
int imr_ifindex;
/* 如:
if_nametoindex():名字转换成索引号
unsigned int if_nametoindex(const char *ifname);
*/
};
/* 大整数 Internet address. */
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};
离开多播组:
IP_ADD_SOURCE_MEMBERSHIP (since Linux 2.4.22 / 2.5.68)
IP_MULTICAST_IF (since Linux 1.2) 创建多播组
代码演示:
/* proto.h */
#ifndef PROTO_H__
#define PROTO_H__
#include <stdint.h>
#define MGROUP "224.2.2.2"
#define RCVER_PORT "2989"
#define NAMESIZE 13
struct msg_st
{
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed));
#endif
/* rcver.c 服务端接收端*/
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <net/if.h>
#include "proto.h"
#define IPSTRSIZE 128
int main()
{
int sd;
struct sockaddr_in laddr,raddr; //协议族
struct msg_st rbuf;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//设置 加入多播组需要的信息
struct ip_mreqn mreq;
inet_pton(AF_INET,MGROUP, &mreq.imr_multiaddr);
inet_pton(AF_INET,"0.0.0.0", &mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("ens33");
/*对指定Socket sd 的IPPROTO_IP层面 的IP_ADD_MEMBERSHIP属性进行设置,表示加入一个多播组,mreq参数为加入多播组需要的信息。*/
if(setsockopt(sd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq)) < 0)
{
perror("setsockopt()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(RCVER_PORT));
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
raddr_len = sizeof(raddr);
while(1)
{
if(recvfrom(sd,&rbuf,sizeof(rbuf),0,(void *)&raddr,&raddr_len) < 0)
{
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("-----MESSAGE FROM:%s:%d-------\n",ipstr,ntohs(raddr.sin_port));
printf("NAME:%s\n",rbuf.name);
printf("MATH:%d\n",ntohl(rbuf.math));
printf("CHINESE:%d\n",ntohl(rbuf.chinese));
}
close(sd);
exit(0);
}
/* snder.c 客户端 发送端 */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <net/if.h>
#include "proto.h"
#define IPSTRSIZE 128
int main(int argc,char *argv[])
{
int sd;
struct sockaddr_in raddr;
struct msg_st sbuf;
//创建Socket, 即确定传输协议族,传输协议,传输方式
sd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
//bind();
//设置 创建多播组需要的信息
struct ip_mreqn mreq;
inet_pton(AF_INET,MGROUP, &mreq.imr_multiaddr);
inet_pton(AF_INET,"0.0.0.0", &mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("ens33");
/*对指定Socket sd 的IPPROTO_IP层面 的IP_MULTICAST_IF属性进行设置,表示创建一个多播组。mreq参数为创建多播组需要的信息。*/
if(setsockopt(sd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq)) < 0)
{
perror("setsockopt()");
exit(1);
}
//struct msg_st 要发送的数据
memset(&sbuf,'\0' ,sizeof(sbuf));
strcpy(sbuf.name,"Alan");
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(RCVER_PORT));
inet_pton(AF_INET,MGROUP,&raddr.sin_addr);
if(sendto(sd,&sbuf,sizeof(sbuf),0,(void *)&raddr,sizeof(raddr)) < 0)
{
perror("sendto()");
exit(1);
}
puts("OK");
close(sd);
exit(0);
}
13.6 流式套接字传输数据
#SOCK_STREAM
$ nc ip port #发送信号
$ talnet ip port
特点:以字节为单位进行传输
步骤解析:
C端:主动端
- 获取socket
- 给socket取得地址(可省略),系统会自动分配
- connect 请求/发送连接
- recv 收/发消息
- close 关闭
S端:被动端,先运行起来,等着收包
- 获取 socket
- 给socket取得地址 bind() 绑定IP地址和端口
- listen 置位监听模式
- accept 接收连接
- send 收/发消息
- close 关闭
listen()
函数功能:监听一个SOCKET
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数
-
sockfd:
-
backlog:原本是指 半连接池的大小,原本是指可以存放多少半连接状态的节点。但是由于因为半连接洪水的攻击,已经放弃了半连接池,所以 backlog 现在变成了指 S端能够承受建立全连接的C端的最大数量。
accept()
函数功能:在SOCKET上建立连接
AF_INET 协议族中的 协议地址类型为 struct sockaddr_in
注意:不同的协议族 来 绑定自己这端的地址 所用的结构体是不一样的。所以是不存在 struct sockaddr 类型的。所以我们的处理方式是:我们用的是哪一个协议族,就把该协议族地址作为addr ,然后再把地址长度写到addrlen
AF_INET see ip(7) --> man 7 ip
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//假错:EINTR EARAIN
/*
注意 :IP地址和端口,是需跟着网络一起发送的。代表自己的身份
*/
struct sockaddr_in
{
//协议族 address family: AF_INET
sa_family_t sin_family;
//需要的端口
in_port_t sin_port;
//IP地址 并非点分式,而是大整数internet address ,用的时候需要格式转换:inet_pton()
struct in_addr sin_addr;
};
参数:
- sockfd:要连接的套接字描述符
- addr:对端地址信息 (对应哪个协议族就用哪个结构体 struct sockaddr_in)
- addrlen:对端地址长度
返回值: 如果成功,这些系统调用将返回一个非负整数,它是所接受套接字的描述符。出现错误时,返回-1,并适当地设置errno。
connect()
函数功能:建立连接socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
- addr:
- addrlen:
sprintf()
可以把整数转换成字符串
int sprintf(char *str, const char *format, ...);
int len;
len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
“代码演示:流式套接字”
/* proto.h 协议 */
#ifndef PROTO_H__
#define PROTO_H__
#include <stdint.h>
#define SERVERPORT "3989"
#define FMT_STAMP "%lld\n"
#endif
/* server.c 接收端*/
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <net/if.h>
#include <errno.h>
#include <time.h>
#include "proto.h"
#define IPSTRSIZE 128
#define BUFSIZE 1024
void server_job(int sd)
{
char buf[BUFSIZE];
int len;
//将时间戳 以 FMT_STAMP格式 存储到 buf中
len = sprintf(buf,FMT_STAMP,(long long)time(NULL));
/*if error*/
if(send(sd,buf,len,0) < 0)
{
perror("send()");
exit(1);
}
return ;
}
int main()
{
int sd,newsd;
struct sockaddr_in laddr,raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
/*取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,*/
sd = socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP,IPPROTO_SCTP*/);
if(sd < 0)
{
perror("socket()");
exit(1);
}
/*解决端口释放问题:
添加属性 : SO_REUSEADDR,表明再次 bind()的时候,如果发现 端口没有释放,该属性会马上释放该端口,并 且让我们绑定成功。*/
/*对指定Socket sd 的SOL_SOCKET层面 的 SO_REUSEADDR属性进行设置,即打开该属性,如果发现 端口没有 释放,该属性会马上释放该端口,并且让我们绑定成功。*/
int val = 1;
if(setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val)) < 0)
{
perror("setsockopt()");
exit(1);
}
/* 设置 AF_INET 协议族地址信息结构体,AF_INET 协议族中的 协议地址类型为 struct sockaddr_in */
laddr.sin_family = AF_INET;
/* 设置端口为3989,因为需要将自己的地址信息(包括端口信息)发出去,所以需要注意字节序问题,即 从主机发向网络,htons */
laddr.sin_port = htons(atoi(SERVERPORT));
// 设置IP
inet_pton(AF_INET,"0.0.0.0",&laddr.sin_addr);
/*给SOCKET绑定一个地址,关联到目标协议族地址信息结构体,即绑定IP,端口
(void *)&laddr, 实际并没有struct sockaddr类型,所以暂时强转为 void *
*/
if(bind(sd,(void *)&laddr,sizeof(laddr)) < 0)
{
perror("bind()");
exit(1);
}
//将socket置为监听模式, 承受C端最大数量为200
if(listen(sd,200) < 0)
{
perror("listen()");
exit(1);
}
/* !!!! */
raddr_len = sizeof(raddr);
while(1)
{
/*接受连接,保存对端地址信息,如果成功,将返回一个非负整数,它是所接受套接字的描述符,即newsd
默认为阻塞等待连接*/
newsd = accept(sd, (void *)&raddr, &raddr_len);
if(newsd < 0)
{
if(errno == EINTR || errno == EAGAIN)
continue;
perror("accept()");
exit(1);
}
//将接收到的对端地址中IP信息 从大整数 转换为 点分式 保存到ipstr数组
inet_ntop(AF_INET,&raddr.sin_addr,ipstr,IPSTRSIZE);
printf("-----MESSAGE FROM:%s:%d-------\n",ipstr,ntohs(raddr.sin_port));
server_job(newsd);
close(newsd);
}
close(sd);
exit(0);
}
注意:若不加这句 setsockopt(sd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val))
在 CTL+C 结束刚刚运行的 server端进程后,立刻再次运行 server端进程,会提示
Address already in use再次 netstat -ant 后会显示 3989端口 处于 TIME_WAIT状态,表明该端口还在工作当中。问题就在于 之前 CTL+C 结束进程,用信号将进程终止,但是server端 最后的 close(sd) 并没有执行到,也就是没有正常结束,没有去刷新该刷新的流,也没有将当前的SOCKET 和 端口释放,所以紧随其后的再次运行 server进程会提示 端口busy,就是因为上一次结束没有正常释放端口, 而等一会儿就可以继续申请3989端口,这是因为内核发现了 3989端口 对应的 socket已经被异常终止了,内核就会帮忙释放掉 3989端口。这样就可以再次运行server服务了。
/* client.c 客户端 */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <net/if.h>
#include <errno.h>
#include <time.h>
#include "proto.h"
#define IPSTRSIZE 128
#define BUFSIZE 1024
int main(int argc ,char *argv[])
{
int sd;
struct sockaddr_in raddr;
char data[1024];
//取得SOCKET, 用 AF_INET协议族中 默认支持流式套接字的协议(IPPROTO_TCP) 来完成流式套接字传输,
sd = socket(AF_INET,SOCK_STREAM,0);
if(sd < 0)
{
perror("socket");
exit(1);
}
//bind
//设置对端地址信息,即设置S端地址信息
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET,argv[1],&raddr.sin_addr);
//建立连接
if(connect(sd,(void *)&raddr,sizeof(raddr)) < 0 )
{
perror("connect()");
exit(1);
}
//接收数据
recv(sd,&data,strlen(data),0);
printf("stamp = %lld\n",(long long)atoi(data));
close(sd);
}
效果展示
xiaolei@xiaolei:~/apue/ipc/socket/stream/basic$ ./client 192.168.5.133
stamp = 163212
xiaolei@xiaolei:~/apue/ipc/socket/stream/basic$ ./client 127.0.0.1
stamp = 163212
xiaolei@xiaolei:~/apue/ipc/socket/stream/basic$ ./server
^[[A-----MESSAGE FROM:192.168.5.133:33320-------
-----MESSAGE FROM:127.0.0.1:42310-------
xiaolei@xiaolei:~/apue/ipc/socket/dgram/bcast$ netstat -ant
激活Internet连接 (服务器和已建立连接的)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:3989 0.0.0.0:* LISTEN