文章目录
- 网络结构模式
- MAC地址、IP地址、端口
- 网络模型
- 协议
- 网络通信的过程
- socket介绍
- 字节序
- 字节序转换函数
- socket地址
- IP地址转换函数(字符串IP地址转换成整数 ,主机、网络字节序的转换)
- TCP通信流程
- socket函数
- bind函数
- listen函数
- accept函数
- connect函数
- write & read函数
- TCP通信实现(服务器端)
- TCP通信实现(客户端)
- 服务器端客户端通信结果展示
- 回射服务器+自定义内容键盘无限次输入
- TCP三次握手
- 滑动窗口
- TCP四次挥手
- 多进程实现并发服务器
- 多线程实现并发服务器
- TCP状态转换(三次握手和四次挥手)
- 半关闭、端口复用
- IO多路复用简介(I/O多路转接)
- select API介绍
- select代码编写
- poll API介绍及代码编写
- epoll API介绍
- epoll代码编写
- epoll的两种工作模式
- UDP通信实现
- 广播
- 组播(多播)
- 本地套接字通信
网络结构模式
C/S结构
优缺点
B/S结构
优缺点
MAC地址、IP地址、端口
MAC地址
MAC地址6个字节
网卡的物理地址
IP地址
IP四个字节
MAC地址具有唯一性,是永远不变的,它就像身份证号,用来唯一确认一台电脑。而IP地址是随电脑所在地域的不同而进行改变的,它像带有邮编的住址信息,IP地址前面的部分就表示了所处的子网。
IP地址编址方式
A类IP地址
用于广域网
A类IP网络地址是00000001——01111110,即1——126
可使用的网络号126个,也就是27-2个【减2的原因由于网络地址全0的IP地址是保留地址意思为"本网络",而网络号为127(即01111111)保留作为本机软件回路测试之用】
B类IP地址
用于城际网络
每个B类地址可连接65534(216 - 2, 因为主机号的各位不能同时为0,1)台主机,Internet有16384(214)个B类地址。
可以容纳256-2=254台主机
C类IP地址
小规模局域网
D类IP地址
特殊的网址
主机号全0为子网网络地址,主机号全1为子网广播地址,这两个地址在任何网络中都不能分配给主机用
子网掩码
端口
端口号两个字节
进程号是变化的,所以网络通信是根据唯一的端口号标识
端口类型
网络模型
OSI 七层参考模型
TCP/IP 四层模型
四层介绍
数据都要通过物理层进行传输的,物理层通过比特流(可以理解为高低电平分别表示1 0刚好与二进制10对应),数据链路层的数据格式为帧,寻找MAC地址(物理地址),网络层的数据的格式是报文,寻找IP地址,所以通过IP地址就能将网络中的不同主机之间建立连接(会进行路由选择,选择路径嘛),传输层锁定端口(端口标识了进程),就可以找到相应的进程,其实计算机间在网络间通信说白了是不同主机间进程的通信,进程找到了,如何通信就涉及到了通信协议TCP、UDP(数据传输)…然后就到了会话层,传输层找到了要传输的两端,会话层就进行连接,打通这条道,建立通路。表示层就更好理解了,将计算机能识别的东西转换成人能识别的,主要通过解释、压缩、解压缩,加密(保证数据安全)什么的。。应用层就是提供网路服务了。。。。 这一切都是为了给网络层的功能铺路的。
协议
- 语义是解释控制信息每个部分的意义。它规定了需要发出何种控制信息,以及完成的动作与做出什么样的响应。
- 语法。语法是用户数据与控制信息的结构与格式,以及数据出现的顺序。
- 时序。时序是对事件发生顺序的详细说明。(也可称为“同步”)。
语义表示要做什么,语法表示要怎么做,时序表示做的顺序
常见协议
ssh
UDP协议
TCP协议
IP协议
以太网帧协议
ARP协议
arp是根据IP找到mac地址
cmd终端输入arp -a指令
ARP协议:通过ip地址查找mac地址
主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
arp地址解析协议的工作原理:
每台主机在其ARP缓存区中维护一个arp列表,当源主机想发送数据给目的主机的时候,先在自己的arp列表中查找是否有该mac地址,如果有就之间将数据发送给目的主机,如果没有就向本地网段发送一个ARP请求广播包来查询目的之举对应的mac地址。
当本地网络中的所有主机都接受到该arp数据包时,首先检查数据包中的IP是否是自己的IP地址,如果不是,则忽略该数据包。如果是,取出数据包中的源主机的IP和MAC地址,并写入自己的arp列表中,并将自己的mac地址发送给源主机。源主机收到相应包之后,先将目的主机中的IP和mac写入自己的arp列表中,并将数据发送给目的主机。如果没有响应包,则表示ARP查询失败。【广播发送ARP请求,单播发送响应】
arp协议在TCP/IP模型中属于IP层(网络层),在OSI模型中属于链路层
ff:ff:ff:ff:ff:ff表示给网段中所有机器发送ARP请求
网络通信的过程
封装
分用
当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing)。分用是依靠头部信息中的类型字段实现的。
图解网络通信过程
详情可看这篇文章
ip地址查询dns服务器获得,然后通过ip找到子网,子网进行一个广播找到指定目的主机,目的主机返回mac地址,后面就知道mac地址了
socket介绍
字节序
简介
字节序举例
检测本机的字节序类型
byteorder.c
/*
字节序:字节在内存中存储的顺序。
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
*/
// 通过代码检测当前主机的字节序
#include <stdio.h>
union tes {
short value; // 2字节
char bytes[sizeof(short)]; // bytes[2]
}test;
int main() {
test.value = 0x0102;
if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
printf("大端字节序\n");
} else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
printf("小端字节序\n");
} else {
printf("未知\n");
}
return 0;
}
字节序转换函数
因为主机为小端,所以htonl()会将数据进行转化。使用ntohl()时默认传入参数为大端,且当前主机为小端,也会发生转化。所以两次结果一致。
bytetrans.c
/*
网络通信时,需要将主机字节序转换成网络字节序(大端),
另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
*/
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// htons 转换端口
// 端口两个字节
unsigned short a = 0x0102;
printf("a : 0x%x\n", a);
// 小端转大端
unsigned short b = htons(a);
printf("b : 0x%x\n", b);
printf("========================================\n");
// htonl 转换IP
// IP四个字节
char buf[4] = {192, 168, 1, 100};
int num = *(int *)buf;
int sum = htonl(num);
unsigned char * p = (char *)∑
printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3));
printf("========================================\n");
// ntohl
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char * p1 = (unsigned char*)&sum1;
printf("%d %d %d %d\n", *p1, *(p1 + 1), *(p1 + 2), *(p1 + 3));
// ntohs
unsigned short a1 = 0x0304;
printf("a1 : 0x%x\n", a1);
unsigned short b1 = ntohs(a1);
printf("b1 : 0x%x\n", b1);
return 0;
}
关于printf的巧妙用法
test.c
#include <stdio.h>
#include <arpa/inet.h>
int main() {
unsigned char ip[] = {192, 168, 0, 1};
int *p = (int*)ip;
*p = htonl(*p);
for (int i = 0; i < 4; ++i)
printf("%d%c", ip[i], " \n"[i == 3]);
return 0;
}
socket地址
// socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个
socket地址。
// 客户端 -> 服务器(IP, Port)
通用 socket 地址
专用 socket 地址
IP地址转换函数(字符串IP地址转换成整数 ,主机、网络字节序的转换)
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:
inet_aton函数
man inet_aton
inet_pton函数
man inet_pton
inet_ntop函数
man inet_ntop
小案例
iptrans.c
/*
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
*/
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// 创建一个ip字符串,点分十进制的IP地址字符串
char buf[] = "192.168.1.4";
unsigned int num = 0;
// 将点分十进制的IP字符串转换成网络字节序的整数
inet_pton(AF_INET, buf, &num);
unsigned char * p = (unsigned char *)#
printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3));
// 将网络字节序的IP整数转换成点分十进制的IP字符串
char ip[16] = "";
const char * str = inet_ntop(AF_INET, &num, ip, sizeof(ip));
printf("str: %s\n", str);
printf("ip: %s\n", ip);
printf("%d\n", ip == str);
return 0;
}
TCP通信流程
服务器端通信流程
客户端通信流程
客户端没有绑定的话系统会自动分配一个socket, 而服务端必须绑定,因为send()要求有目的地址作为参数。类似于打电话的时候不需要知道自己的号码,但一定知道对方的号码
socket函数
协议族
man socket
协议类型
套接字具有指定的类型,该类型指定通信语义。 目前定义的类型有
protocol
该协议指定与套接字一起使用的特定协议。 通常,在给定协议族中只存在一个协议来支持特定的套接字类型,在这种情况下,协议可以指定为 0。然而,可能存在许多协议,在这种情况下必须以这种方式指定特定协议。 要使用的协议号特定于要在其中进行通信的“通信域”; 参见协议(5)。 有关如何将协议名称字符串映射到协议编号的信息,请参阅 getprotoent(3)。
返回值
bind函数
man 2 bind
返回值
listen函数
man 2 listen
Listen() 将 sockfd 引用的套接字标记为被动套接字,即,作为将用于使用accept(2) 接受传入连接请求的套接字。
sockfd 参数是一个文件描述符,引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字。
backlog 参数定义 sockfd 的挂起连接队列可以增长的最大长度。 如果连接请求在队列已满时到达,则客户端可能会收到带有 ECONNREFUSED 指示的错误,或者如果底层协议支持重传,则可能会忽略该请求,以便稍后重新尝试连接成功。
accept函数
man 2 accept
socklen_t就是unsigned int
connect函数
man 2 connect
write & read函数
TCP通信实现(服务器端)
server.c
// TCP 通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端的信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", clientIP, clientPort);
// 5.通信
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1) {
perror("read");
exit(-1);
} else if(num > 0) {
printf("recv client data: %s\n", recvBuf);
} else if(num == 0) {
// 表示客户端断开连接
printf("client closed...");
break;
}
char *data = "hello, I am server!";
// 给客户端发送数据
write(cfd, data, strlen(data));
}
// 关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
TCP通信实现(客户端)
客户端绑定的端口是随机的,客户端里面指定的9999是去连接服务器的这个端口。
client.c
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3.通信
char recvBuf[1024] = {0};
while(1) {
char *data = "hello, I am client!!";
// 给客户端发送数据
write(fd, data, strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data: %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...\n");
break;
}
}
// 关闭连接
close(fd);
return 0;
}
服务器端客户端通信结果展示
回射服务器+自定义内容键盘无限次输入
server.c
// TCP 通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(3333);
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端的信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", clientIP, clientPort);
// 5.通信
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1) {
perror("read");
exit(-1);
} else if(num > 0) {
printf("recv client data: %s\n", recvBuf);
} else if(num == 0) {
// 表示客户端断开连接
printf("client closed...\n");
break;
}
// char *data = "hello, I am server!";
// 给客户端发送数据
write(cfd, recvBuf, strlen(recvBuf));
memset(recvBuf, 0, 1024);
}
// 关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
client.c
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(3333);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3.通信
char recvBuf[1024] = {0};
char data[1024] = {0};
while(1) {
printf("please input data:\n");
int res = scanf("%[^\n]", data);
// 消除回车影响
getchar();
if(res != -1) {
// 给客户端发送数据
write(fd, data, strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data: %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...\n");
break;
}
} else {
perror("scanf");
exit(-1);
}
memset(recvBuf, 0, 1024);
memset(data, 0, 1024);
}
// 关闭连接
close(fd);
return 0;
}
TCP三次握手
前两次握手是带不了数据的,最后一次握手其实是可以携带数据的。不过一般我们不去考虑复杂的情况
总结一下:为了确保双方都有发送与接收的能力,第一次客户端给服务端发送请求,服务端知道客户端有发送的能力,于是第二次回信;当客户端收到回信后,知道服务端有接受与发送的能力,且为了让服务器知道自己有接收的能力,于是第三次发送信息,在服务器接收到信息时知道客户端有接收的能力,于是建立好了连接
滑动窗口
这个图里面的ACK应该是ack ,而不是ACK标志位
mss字段在TCP数据包的首部,是告知对方自己能一次能接收的最大数据长度。
fast sender先告诉slow receiver,我的滑动窗口是4096,一次能接收的最大长度为1460。
然后slow reciever告诉fast sender,我的滑动窗口是6144,一次能接收的最大长度为1024。
所以后面fast sender给slow reciever发送的数据包都是1024字节
MSS值(MSS = MUT - IP首部长度 - TCP首部长度)。该MSS值是为了告知对方最大的发送数据大小。
MUT(Maximum Transmission Unit)是指网络通信中的最大传输单元
TCP四次挥手
这个图里面的ACK应该是ack ,而不是ACK标志位
mss字段在TCP数据包的首部,是告知对方自己能一次能接收的最大数据长度。
fast sender先告诉slow receiver,我的滑动窗口是4096,一次能接收的最大长度为1460。
然后slow reciever告诉fast sender,我的滑动窗口是6144,一次能接收的最大长度为1024。
所以后面fast sender给slow reciever发送的数据包都是1024字节
知乎的四次挥手内容
- 1、A的应用进程先向其TCP发出连接释放报文段(FIN=1,seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。
- 2、B收到连接释放报文段后即发出确认报文段(ACK=1,ack=u+1,seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。
- 3、A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
- 4、B发送完数据,就会发出连接释放报文段(FIN=1,ACK=1,seq=w,ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。
- 5、A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL(最大报文段生存时间)后,A才进入CLOSED状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。
第四次挥手为什么要等待2MSL?
- 保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在2MSL时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到CLOSED状态,若A在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到CLOSED状态。
- 防止已失效的连接请求报文段出现在本连接中。A在发送完最后一个ACK报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。
为什么是四次挥手?
多进程实现并发服务器
TCP通信并发
案例
server_process.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recycleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
} else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0) {
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main() {
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recycleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);
// 创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 不断循环等待客户端连接
while(1) {
// 接受连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) {
continue;
}
perror("accept");
exit(-1);
}
// 每一个连接进来, 创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIP[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is %s, port is %d\n", cliIP, cliPort);
// 接受客户端发来的数据
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int len = read(cfd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv client data: %s\n", recvBuf);
} else if(len == 0) {
// 表示客户端断开连接
printf("client closed...\n");
break;
}
// char *data = "hello, I am server!";
// 给客户端发送数据
write(cfd, recvBuf, len);
memset(recvBuf, 0, 1024);
}
close(cfd);
// 退出当前子进程
exit(0);
}
}
// 关闭文件描述符
close(lfd);
return 0;
}
client.c
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3.通信
char recvBuf[1024] = {0};
// char data[1024] = {0};
int i = 0;
while(1) {
// printf("please input data:\n");
sprintf(recvBuf, "data : %d\n", i++);
// int res = scanf("%[^\n]", data);
// 消除回车影响
// getchar();
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
memset(recvBuf, 0, 1024);
}
// 关闭连接
close(fd);
return 0;
}
如果在不通过信号,直接用waitpid的话,如果不加while(1),执行waitpid的时候没有子进程,子进程无法回收,加上waitpid一次只能回收一个子进程,所以要用while(1),轮询的去回收子进程的资源。不通过信号,父进程就没办法执行别的事情了,包括继续监听,创建子进程…
信号就好像一个程序中断服务,处理完回收子进程的事情再恢复现场继续监听…
父进程中直接调用 wait/waitpid 有个问题,就是可能一直没有新客户端连接,父进程会一直阻塞在 accept() 处,无法执行到 wait 处。所以要设置信号捕捉。这样父进程阻塞在 accept() 处时会被打断去回收子进程。
案例修改版(设置sa_flag 为SA_RESTART)
man 7 signal
也可以设置sa_flag 为SA_RESTART,这样阻塞函数被打断处理信号完成后,会重新accept。
server_process.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recycleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
} else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0) {
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main() {
struct sigaction act;
act.sa_flags = SA_RESTART;
sigemptyset(&act.sa_mask);
act.sa_handler = recycleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);
// 创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 不断循环等待客户端连接
while(1) {
// 接受连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
// if(errno == EINTR) {
// continue;
// }
perror("accept");
exit(-1);
}
// 每一个连接进来, 创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIP[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is %s, port is %d\n", cliIP, cliPort);
// 接受客户端发来的数据
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int len = read(cfd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv client data: %s\n", recvBuf);
} else if(len == 0) {
// 表示客户端断开连接
printf("client closed...\n");
break;
}
// char *data = "hello, I am server!";
// 给客户端发送数据
write(cfd, recvBuf, len);
memset(recvBuf, 0, 1024);
}
close(cfd);
// 退出当前子进程
exit(0);
}
}
// 关闭文件描述符
close(lfd);
return 0;
}
client.c
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3.通信
char recvBuf[1024] = {0};
// char data[1024] = {0};
int i = 0;
while(1) {
// printf("please input data:\n");
sprintf(recvBuf, "data : %d\n", i++);
// int res = scanf("%[^\n]", data);
// 消除回车影响
// getchar();
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
memset(recvBuf, 0, 1024);
}
// 关闭连接
close(fd);
return 0;
}
1.wait放在父进程中,阻塞,无法接收另外一个服务端
2.如果不用信号,使用waitpid的话,如果不加while(1),执行waitpid一次只能回收一个子进程,但是加上while(1),可以轮询的去回收子进程的资源。但是不通过信号,父进程就没法执行别的事情,包括继续监听,创建子进程
errno的作用
案例完善版
server_process.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recycleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
} else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0) {
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main() {
struct sigaction act;
act.sa_flags = SA_RESTART;
sigemptyset(&act.sa_mask);
act.sa_handler = recycleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);
// 创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 不断循环等待客户端连接
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
// if(errno == EINTR) {
// continue;
// }
perror("accept");
exit(-1);
}
// 每一个连接进来, 创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIP[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is %s, port is %d\n", cliIP, cliPort);
// 接受客户端发来的数据
char recvBuf[1024];
while(1) {
// 获取客户端的数据
int len = read(cfd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv client data: %s\n", recvBuf);
} else if(len == 0) {
// 表示客户端断开连接
printf("client closed...\n");
break;
}
// char *data = "hello, I am server!";
// 给客户端发送数据
write(cfd, recvBuf, strlen(recvBuf) + 1);
// memset(recvBuf, 0, 1024);
}
close(cfd);
// 退出当前子进程
exit(0);
}
}
// 关闭文件描述符
close(lfd);
return 0;
}
client.c
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3.通信
char recvBuf[1024] = {0};
// char data[1024] = {0};
int i = 0;
while(1) {
// printf("please input data:\n");
sprintf(recvBuf, "data : %d\n", i++);
// int res = scanf("%[^\n]", data);
// 消除回车影响
// getchar();
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
sleep(1);
}
// 关闭连接
close(fd);
return 0;
}
多线程实现并发服务器
同一个程序内的线程共享文件描述符表
server_thread.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid; // 线程号
};
struct sockInfo sockinfos[128];
void *working(void *arg) {
// 子线程和客户端通信 cfd 客户端的信息 线程号
// 获取客户端的信息
struct sockInfo * pinfo = (struct sockInfo *)arg;
char cliIP[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIP, sizeof(cliIP));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is %s, port is %d\n", cliIP, cliPort);
// 接受客户端发来的数据
char recvBuf[1024];
while(1) {
// 获取客户端的数据
int len = read(pinfo->fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
pthread_exit(NULL);
} else if(len > 0) {
printf("recv client data: %s\n", recvBuf);
} else if(len == 0) {
// 表示客户端断开连接
pinfo->fd = -1;
printf("client closed...\n");
break;
}
// char *data = "hello, I am server!";
// 给客户端发送数据
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
// memset(recvBuf, 0, 1024);
}
close(pinfo->fd);
return NULL;
}
int main() {
// 创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1) {
struct sockaddr_in arg;
int len = sizeof(arg);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&arg, &len);
if(cfd == -1) {
// if(errno == EINTR) {
// continue;
// }
perror("accept");
exit(-1);
}
// 堆内存中开辟数据
struct sockInfo *pinfo;
for(int i = 0; i < max; i++) {
// 从这个数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {
sleep(1);
i = -1;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr, &arg, len);
// 创建子线程
pthread_create(&pinfo->tid, NULL, working, pinfo);
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
client.c
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3.通信
char recvBuf[1024] = {0};
// char data[1024] = {0};
int i = 0;
while(1) {
// printf("please input data:\n");
sprintf(recvBuf, "data : %d\n", i++);
// int res = scanf("%[^\n]", data);
// 消除回车影响
// getchar();
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
sleep(1);
}
// 关闭连接
close(fd);
return 0;
}
pthread_exit(NULL)和exit(-1)的区别
TCP状态转换(三次握手和四次挥手)
time_wait状态是主动关闭的一方才有的
红色是客户端状态改变,绿色是服务端状态改变
为什么是四次挥手而不是三次挥手
因为是一方主动断开的,而另外一方可能还有数据没有传过去,所以ACK和FIN要分开
2MSL(Maximum Segment Lifetime)
结论
灵魂拷问:
第一次挥手丢失,会发生什么?
答:主动断开方重传FIN请求报文
第二次挥手丢失,会发生什么?
答:由于ACK报文不会重传,所以主动断开方会重传FIN报文
第三次挥手丢失,会发生什么?
答:被动断开方会重传FIN报文
第四次挥手丢失,会发生什么?
答:同第三次挥手
半关闭、端口复用
半关闭
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。
从程序的角度,可以使用 API 来控制实现半连接状态:
查看网络相关信息的命令netstat
SFTP上传文件
recv函数
man 2 recv
send函数
man 2 send
只运行服务器,客户端未开启时
服务器和客户端都开启
端口复用
setsockopt函数
man 2 setsockopt
端口复用使用的案例代码
tcp_server.c
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
// int optval = 1;
// setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
return -1;
}
// 监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
return -1;
}
// 接收客户端连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1) {
perror("accpet");
return -1;
}
// 获取客户端信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
// 输出客户端的信息
printf("client's ip is %s, and port is %d\n", cliIp, cliPort );
// 接收客户端发来的数据
char recvBuf[1024] = {0};
while(1) {
int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
if(len == -1) {
perror("recv");
return -1;
} else if(len == 0) {
printf("客户端已经断开连接...\n");
break;
} else if(len > 0) {
printf("read buf = %s\n", recvBuf);
}
// 小写转大写
for(int i = 0; i < len; ++i) {
recvBuf[i] = toupper(recvBuf[i]);
}
printf("after buf = %s\n", recvBuf);
// 大写字符串发给客户端
ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
if(ret == -1) {
perror("send");
return -1;
}
}
close(cfd);
close(lfd);
return 0;
}
tcp_client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
while(1) {
char sendBuf[1024] = {0};
// fgets会阻塞
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
}
close(fd);
return 0;
}
先关闭服务端 那么服务端进入到FIN_WAIT_2(这个状态会持续1-2分钟) 此时客户端写数据 不会发生错误 write的返回值大于0 但是在下面read会返回0(因为服务端已经关闭) 所以客户端也close 就关闭了
IO多路复用简介(I/O多路转接)
I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。
BIO模型
阻塞等待
NIO模型
非阻塞, 忙轮询
IO多路转接技术
第一种:select / poll
第二种:epoll
select API介绍
状态是 1 还是 0 是每个 fd 都要检测一次的。 只是说 状态为0的fd文件就不用去检测缓冲区。只检测状态为1的对应文件的缓冲区,如果没有数据到达,就把比特位改动为0。
man 2 select
select()工作过程分析
具体分析
select代码编写
fd_set本质是个long int类型的数组
select函数IO多路复用小案例实现
select调用前rdset的某位fd为1表示我们希望内核帮我们检测该fd对应的接收缓存。select调用后rdset对应的fd为1表示该接收缓存接收到数据了,为0表示没接收到数据
select.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
// 绑定所有网卡上的ip
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 创建一个fd_set的集合,存放的是需要检测的文件描述符
// 给内核修改的是tmp, 用户态用rdset用来进行FD_SET、FD_ZERO等操作
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1) {
tmp = rdset;
// 调用select系统函数,让内核帮检测哪些文件描述符有数据
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1) {
perror("select");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
if(FD_ISSET(lfd, &tmp)) {
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
// 将新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
for(int i = lfd + 1; i <= maxfd; i++) {
if(FD_ISSET(i, &tmp)) {
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(i);
FD_CLR(i, &rdset);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
sleep(1);
// usleep(1000);
}
close(fd);
return 0;
}
select的缺点
poll API介绍及代码编写
poll函数
man 2 poll
poll多路复用
poll多路复用小案例
但这个代码有个小问题,比如accept建立连接了,但是因fds数组已满,accept返回的文件描述符无法加入到 fds 中,那么下一次循环时该文件描述符的值(代码中的 cfd )已经被丢弃了。
所以这个地方应该先去看数组中有没有可用的,有的话就accpet连接,没有就下一次在处理
poll.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
// 绑定所有网卡上的ip
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 初始化检测的文件描述符数组
struct pollfd fds[1024];
for(int i = 0; i < 1024; i++) {
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;
while(1) {
// 调用poll系统函数,让内核帮检测哪些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if(ret == -1) {
perror("poll");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
if(fds[0].revents & POLLIN) {
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
// 将新的文件描述符加入到集合中
for(int i = 1; i < 1024; i++) {
if(fds[i].fd == -1) {
fds[i].fd = cfd;
fds[i].events = POLLIN;
// 更新最大的文件描述符的索引
nfds = nfds > i ? nfds : i;
break;
}
}
}
for(int i = 1; i <= nfds; i++) {
if(fds[i].revents & POLLIN) {
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(fds[i].fd);
fds[i].fd = -1;
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
sleep(1);
// usleep(1000);
}
close(fd);
return 0;
}
epoll API介绍
epoll()多路复用
事件驱动才是检测高效的原因,红黑树节点上注册有回调函数,事件到来后执行回调函数
epoll_create函数
man 2 epoll_create
epoll_ctl函数
man 2 epoll_ctl
epoll_wait函数
epoll代码编写
curfd == lfd代表的是服务器监听到有新的客户端连接,因为lfd是服务器负责监听的文件描述符
epoll.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
// 绑定所有网卡上的ip
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
// sleep(1);
// usleep(1000);
}
close(fd);
return 0;
}
epoll的两种工作模式
LT 模式 (水平触发)
案例代码
epoll_lt.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
// 绑定所有网卡上的ip
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
fgets(sendBuf, sizeof(sendBuf), stdin);
// sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
// sleep(1);
// usleep(1000);
}
close(fd);
return 0;
}
ET 模式(边沿触发)
文件句柄就是文件描述符
案例代码
epoll_et.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
// 绑定所有网卡上的ip
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
// 设置cfd属性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 设置边沿触发
epev.events = EPOLLIN | EPOLLET;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & (EPOLLOUT | !EPOLLIN)) {
continue;
}
// 循环读取出所有数据
char buf[5];
int len = 0;
while((len = read(curfd, buf, sizeof(buf))) > 0) {
// 打印数据
printf("recv data: %s\n", buf);
// write(STDOUT_FILENO, buf, len);
write(curfd, buf, len);
}
if(len == 0) {
printf("\nclient closed..., fd is : %d\n", curfd);
} else if (len == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据暂时不可用,可以等待一段时间后重试或者进行其他操作
// 在非阻塞情况下,数据读完了,我们还继续读,就会产生这个问题
printf("data not ready yet\n");
} else if (errno == ECONNRESET) {
// 连接被对方重置
printf("connection reset by peer\n");
} else {
// 其他错误
perror("read");
exit(-1);
}
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
client.c
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
fgets(sendBuf, sizeof(sendBuf), stdin);
// sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
memset(sendBuf, 0, sizeof(sendBuf));
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
// sleep(1);
// usleep(1000);
}
close(fd);
return 0;
}
UDP通信实现
sendto函数
man 2 sendto
recvfrom函数
man 2 recvfrom
UDP通信实现小案例
udp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
// 2.绑定
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.通信
while(1) {
char recvbuf[128];
char ipbuf[16];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接收数据
recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&cliaddr, &len);
printf("client IP: %s, Prot: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)), ntohs(cliaddr.sin_port));
printf("client send data: %s\n", recvbuf);
// 发送数据
sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr*)&cliaddr, len);
}
close(fd);
return 0;
}
udp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 服务器的地址信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
int num = 0;
// 3.通信
while(1) {
// 发送数据
char sendBuf[128];
sprintf(sendBuf, "hello, I am client%d \n", num++);
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr*)&saddr, sizeof(saddr));
// 接收数据
recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
printf("server send data: %s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
sizeof计算的是分配的数组所占的内存空间的大小,不受里面存储的内容影响;strlen计算的是字符串的长度,以’\0’为字符串结束标志。
这里recvfrom要接收一段未知长度的字符串,所以是指定用于接收的缓冲区及缓冲区大小;而sendto要发送一段字符串,并指定发送数据的长度。
广播
向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
- a.只能在局域网中使用。
- b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。
广播案例实现
按照接收方需要绑定端口来理解;这个时候服务器是发起方,客户端才是接收方,需要绑定接收端口;
这里服务端是主动给别人发送数据了,不需要手动绑定一个端口了,但是底层肯定还会分配一个端口给他的
bro_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.设置广播属性
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
// 3.创建一个广播的地址
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "10.0.23.255", &cliaddr.sin_addr.s_addr);
// 3.通信
int num = 0;
while(1) {
char sendBuf[128];
sprintf(sendBuf, "hello, client...%d\n", num++);
// 发送数据
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
printf("广播的数据: %s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
bro_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.通信
while(1) {
char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("server say: %s\n", buf);
}
close(fd);
return 0;
}
server只发送数据,要先开启广播设置,指定发送数据目的地client是局域网内的广播地址,并指定广播端口,但它本身不需要bind,因为它自己的端口号是啥(不bind就内核分配)不影响client的端口
client的IP地址必须是局域网的广播地址才行(所以可以使用INADDR_ANY),但端口必须是server广播使用的端口(所以一定要绑定广播端口)
组播(多播)
被动接收的一端都要绑定一个端口,主动发送的一端一般都是系统分配端口
组播地址
设置组播
组播小案例
multi_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.设置多播的属性,设置外出接口
struct in_addr imr_multiaddr;
// 初始化多播地址
inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));
// 3.初始化客户端的地址信息
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr);
// 3.通信
int num = 0;
while(1) {
char sendBuf[128];
sprintf(sendBuf, "hello, client....%d\n", num++);
// 发送数据
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
printf("组播的数据:%s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
multi_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct in_addr in;
// 2.客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
struct ip_mreq opt;
inet_pton(AF_INET, "239.0.0.10", &opt.imr_multiaddr.s_addr);
opt.imr_interface.s_addr = INADDR_ANY;
// 加入到多播组
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &opt, sizeof(opt));
// 3.通信
while(1) {
char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("server say: %s\n", buf);
}
close(fd);
return 0;
}
本地套接字通信
本地套接字通信的流程
服务器端
man 2 socket
客户端
unlink函数
man 2 unlink
本地套接字通信小案例
ipc_server.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>
int main() {
unlink("server.sock");
// 1.创建监听的套接字
int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock");
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 100);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.等待客户端连接
struct sockaddr_un cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
printf("client socket filename: %s\n", cliaddr.sun_path);
// 5.通信
while(1) {
char buf[128];
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("client closed....\n");
break;
} else if(len > 0) {
printf("client say : %s\n", buf);
send(cfd, buf, len, 0);
}
}
close(cfd);
close(lfd);
return 0;
}
ipc_client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <errno.h>
int main() {
unlink("client.sock");
// 1.创建套接字
int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(cfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "client.sock");
int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.连接服务器
struct sockaddr_un seraddr;
seraddr.sun_family = AF_LOCAL;
strcpy(seraddr.sun_path, "server.sock");
ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 4.通信
int num = 0;
while(1) {
// 发送数据
char buf[128];
sprintf(buf, "hello, i am client %d\n", num++);
ret = send(cfd, buf, strlen(buf) + 1, MSG_NOSIGNAL);
if(ret == -1) {
if(errno == EPIPE) {
printf("server closed....\n");
break;
} else {
perror("send");
exit(-1);
}
}
printf("client say : %s\n", buf);
// 接收数据
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("server closed....\n");
break;
} else if(len > 0) {
printf("server say : %s\n", buf);
}
sleep(1);
}
close(cfd);
return 0;
}
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!