目录
一、网络发展史
协议:通信的双方的约定的好的,如何发送数据以及受到数据后如何进行解析的一个规则。
1. ARPAnet阶段
早期的ARPAnet使用网络控制协议(Network Control Protocol,NCP)
不能互联不同类型的计算机和不同类型的操作系统,没有纠错功能
2. TCP/IP两个协议阶段
TCP/IP协议分成了两个不同的协议:
用来检测网络传输中差错的传输控制协议TCP 纠错
专门负责对不同网络进行互联的互联网协议IP 兼容
3. 网络体系结构及OSI开放系统互联模型
网络采用分而治之的方法设计,将网络的功能划
分为不同的模块,以分层的形式有机组合在一起
每层实现不同的功能,其内部实现方法对外部其他
层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务
网络体系结构即指网络的层次结构和每层所使用协议的集合
OSI开放系统互联模型,由ISO国际标准化组织提出的。
该模型共有7层,从上往下说也行,从下往上说也可以,但是顺序不能颠倒
从下往上:物数网传会表应
OSI模型相关的协议已经很少使用,但模型本身非常通用
OSI模型是一个理想化的模型,尚未有完整的实现。
4. TCP/IP协议族(簇)体系结构
TCP/IP协议族是Internet事实上的工业标准。
TCP/IP协议族体系结构共有四层:
应用层、传输层、网络层、链路层。
虽然TCP/IP协议族体系结构只有四层,但是做的事儿和OSI七层是一样的。
TCP/IP与OSI参考模型的对应关系
linux内核的五大功能:
内存管理:内存的分配和回收
进程管理:时间片轮转、上下文切换
文件管理:将一堆01转换成方便人类识别的字符
设备管理:linux一切皆文件、设备驱动的管理
网络管理:网络协议栈的管理
收发数据封包和拆包的过程:
一帧数据说明:
1500是MTU,最大传输单元,超过这个值的数据就会被拆分成多个帧进行发送
MTU的值是可以设置的,我们一般都使用默认值,以太网中MTU默认都是1500
具体以 ifconfig 命令 mtu 后面的值为准
TCP/IP协议族每层常见的协议:
应用层:
HTTP(Hypertext Transfer Protocol) 超文本传输协议
万维网的数据通信的基础
FTP(File Transfer Protocol) 文件传输协议
是用于在网络上进行文件传输的一套标准协议,使用TCP传输
TFTP(Trivial File Transfer Protocol) 简单文件传输协议
是用于在网络上进行文件传输的一套标准协议,使用UDP传输
SMTP(Simple Mail Transfer Protocol) 简单邮件传输协议
一种提供可靠且有效的电子邮件传输的协议
传输层:
TCP(Transport Control Protocol) 传输控制协议
是一种面向连接的、可靠的、基于字节流的传输层通信协议
UDP(User Datagram Protocol) 用户数据报协议
是一种无连接、不可靠、快速传输的传输层通信协议
网络层:
IP(Internetworking Protocol) 网际互连协议
是指能够在多个不同网络间实现信息传输的协议
ICMP(Internet Control Message Protocol) 互联网控制信息协议
用于在IP主机、路由器之间传递控制消息----ping命令使用协议
IGMP(Internet Group Management Protocol) 互联网组管理
是一个组播协议,用于主机和组播路由器之间通信
链路层:
ARP(Address Resolution Protocol) 地址解析协议
通过IP地址获取对方mac地址
RARP(Reverse Address Resolution Protocol) 逆向地址解析协议
通过mac地址获取ip地址
注意:每层使用的协议由下层决定,不能乱用。
二、TCP和UDP的异同
相同点:同为传输层的协议
不同点:
TCP(即传输控制协议)概念:
是一种面向连接的传输层协议,它能提供高可
靠性通信(即数据无误、数据无丢失、数据无失序、数
据无重复到达的通信)
适用情况:
适合于对传输质量要求较高,以及传输大量数据的通信。
在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
UDP(User Datagram Protocol)用户数据报协议
是不可靠的无连接的协议。在数据发送前,
因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)
适合于广播/组播式通信中。
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
三、网络基础及相关的概念
3.1 字节序
不同类型CPU的主机中,内存存储多字节整数序列有两种方法,称为主机字节序(HBO):
小端序(little-endian) - 低序字节存储在低地址
将低字节存储在起始地址,称为“Little-Endian”字节序,Intel、AMD等采用的是这种方式;
大端序(big-endian)- 高序字节存储在低地址
将高字节存储在起始地址,称为“Big-Endian”字节序,由ARM、Motorola等所采用
如何判断自己的主机字节序?
#include <stdio.h>
int main(){
int test = 0x12345678;
char *p = (char *)&test;
if(*p == 0x78){
printf("小端\n");
}else if(*p == 0x12){
printf("大端\n");
}
return 0;
}
如果多台主机在通信时,字节序不一样,则实际接收的数据,就有可能和发送的数据不一致。
所以为了保证不同字节序的主机都能正常的收发数据,就发明了网络字节序(规定为大端序)
也就是说,发送方在发送数据之前,需要将数据转换成网络字节序再发送
接收方接到数据收,也认为是网络字节序的数据,需要转换成主机字节序再处理。
收发什么样的数据需要考虑字节序的问题
1.如果明确知道收发数据的双方字节序一致,可以不转换
2.如果只发送一个字节的数据,可以不考虑
将多字节的数据作为整体发送时,需要考虑字节序(如 short int)
字符串不用转换字节序
如何将主机字节序的数据转换成网络字节序:以4字节为例
int main(int argc, const char *argv[])
{
int num = 0x12345678;
int test = 0x11223344;
char *p_test = (char *)&test;
if(*p_test == 0x44){
char temp = 0;
char *p = (char *)#
char *q = p+3;
temp = *p;
*p = *q;
*q = temp;
p++;
q--;
temp = *p;
*p = *q;
*q = temp;
}
printf("network num:%#x\n", num);//0x78563412
return 0;
}
操作系统给我们提供了转换的函数:
#include <arpa/inet.h>
//h host 主机 n network 网络 to 转换 s 2字节 l 4字节
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
使用实例:以htonl为例 其他同理
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, const char *argv[])
{
int host_num = 0x12345678;
int net_num = htonl(host_num);
printf("host ---> net %#x-->%#X\n", host_num, net_num);
return 0;
}
3.2 socket
socket本来也是用于同主机的进程间通信的
后来又了TCP/IP协议族的加入,才能实现网络通信
socket是什么?
是一个编程接口(就是函数)
是一种特殊的文件描述符 (everything in Unix is a file)
并不仅限于TCP/IP协议
并不仅限于linux
面向连接 (Transmission Control Protocol - TCP/IP)
无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)
socket是内核给我们提供的函数,将复杂的网络通的过程转换成了我们熟悉的IO操作
TCP/IP协议被集成到操作系统的内核中,引入了新型的“I/O”操作,我们操作套接字,
只需要通过传参的方式,来指定想使用的协议即可。
对套接字的read操作,就是在套接字上接收数据
对套接字的write操作,就是向套接字上发送数据
套接字的类型:
流式套接字(SOCK_STREAM)----给TCP使用
提供了一个面向连接、可靠的数据传输服务,数据无差
错、无重复的发送且按发送顺序接收。内设置流量控制,
避免数据流淹没慢的接收方。数据被看作是字节流,
无长度限制。
数据报套接字(SOCK_DGRAM)----给UDP使用
提供无连接服务。数据包以独立数据包的形式被发送,
不提供无差错保证,数据可能丢失或重复,顺序发送,
可能乱序接收。
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问。
3.3IP地址
主机在网络中的编号,这个编号就是IP地址。
IP地址和MAC地址的区别?
IP地址是网络的一个编号
MAC地址是网卡的物理地址,理论上讲应该是全球唯一的,
但是现在有很多虚拟化的技术,已经不保证全球唯一了,但是同一局域网中一定唯一。
MAC地址占用 6个字节,例如 00:0c:29:a1:f9:68
linux中使用 ifconfig 命令可以查看mac地址
win中使用 ipconfig/all 命令可以查看mac地址
局域网内部使用 MAC地址通信,如果数据想走出局域网,就需要用到IP地址了。
IP地址的分类
IPV4 4字节 32bit
IPV6 16字节 128bit
现在企业中大部分使用的还是IPV4,因为有NAT技术的加持,
能很大程度缓解IP地址不够用的问题。
IP地址的表示形式 "192.168.80.10" 这种叫做点分十进制,是一个字符串
计算机中存储IP地址是用的无符号4字节整型。unsigned int
IPV4地址的组成:
由 网络号 和 主机号 组成
IPV4地址分类:
网络号 主机号 规定最高位 范围 使用单位
A 1字节 3字节 0 0.0.0.0 - 127.255.255.255 政府/大公司/学校
B 2字节 2字节 10 128.0.0.0-191.255.255.255 中等规模的公司
C 3字节 1字节 110 192.0.0.0-223.255.255.255 个人
D 1110 [224-239] 组播
E 11110 [240-255] 保留测试用的
其中每个IP地址又可以通过路由器,下发局域网IP地址,
每类IP地址都有专门划分子网的保留段。
子网掩码:
是由一堆连续的1和连续的0组成的。
用来和IP地址取与运算来获取网络号的。
从而能限制某一网段内能容纳的最大主机数。
如,ip地址是192.168.70.8 子网掩码设置成 255.255.255.0
取与运算可以得到的结果:192.168.70.0 ----这是网络号,网络号相同时,才能进行通信
这时,该网段内共有IP地址 256 个:
其中 192.168.70.0 是网络号,是不能占用的
192.168.70.255 是广播的地址,也不能占用
网关设备也需要占用一个IP地址,一般是同一局域网内可用的编号最小的。
192.168.70.1
所以能容纳的主机数:256-1-1-1(网关设备也可以算作一台IP主机) = 254
192.168.70.x 网段将子网掩码设置成 255.255.255.128,同一子网能容纳的最大主机数?
答案:这种方式可以将同一网段再划分成两个子网
第一个子网IP地址范围:192.168.70.0~192.168.70.127
第二个子网IP地址范围:192.168.70.128~192.168.70.255
其中每个子网中有需要减掉 网络号 广播地址 网关地址
所以子网掩码不一定都是255.255.255.0
网关设备的IP地址也不一定是编号最小的。
既然是无符号4字节整型,就涉及字节序的问题。
在网络字节序下,ip地址的存储:
//将点分十进制的字符串 转换成 网络字节序的 无符号四字节整型
in_addr_t inet_addr(const char *cp);
//将网络字节序的无符号四字节整型的ip地址 转换成 点分十进制的字符串
char *inet_ntoa(struct in_addr in);
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, const char *argv[])
{
char str[] = "192.168.80.30";
unsigned int ip = inet_addr(str);
unsigned char *p = (unsigned char *)&ip;
printf("%s --> %d.%d.%d.%d\n", str, *p, *(p+1), *(p+2), *(p+3));
return 0;
}
执行结果:
3.4 端口号
网络通信是不同主机的进程间通信,通过IP地址可以在复杂的网络环境中找到对应的主机
但是还得确定消息发给哪个进程。
如果使用pid来标识进程
第一个问题pid没法手动指定
第二个问题是进程重启时一般pid都会发生变化,不好管理
所以就发明了端口号,用来人为的标识某一个进程。
端口号一般由IANA (Internet Assigned Numbers Authority) 管理
众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用)
我们可以用的端口:1024~49151
动态或私有端口:49152~65535
linux系统端口号的范围 [0-65535] 共计 65536个 使用 unsigned short 存储
实际开发的时候,端口号是由用户指定的,学习阶段我们为了防止冲突
可以使用一些特殊数字的,如 8888 9999 6789 6666 等
linux系统中 /etc/services 中保存的就是当前系统中已经被占用的端口号
常见的服务使用的端口号:
ftp 21
ssh 22
tftp 69
http 80 8080
mysql 3306
四、TCP网络编程
网络编程模型
c/s 模型:客户端服务器模型
b/s 模型:浏览器服务器模型
1.tcp网络流程
服务器流程:
1.创建套接字
2.完善服务器网络信息结构体
3.绑定服务器网络信息结构体
4.让服务器处于监听状态
5.accept阻塞等待客户端连接信号
6.收发数据
7.关闭套接字
客户端流程:
1.创建套接字
2.连接connect
3.收发数据
4.关闭套接字
2.函数
2.1socket
#include <sys/types.h>
#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_PACKET 原始套接字 packet(7)type:套接字的类型
SOCK_STREAM TCP
SOCK_DGRAM UDP使用
SOCK_RAW 原始套接字使用
protocol:附加文件,没有写0
返回值:成功:创建的新套接字(文件描述符)
失败:-1 重置错误码
2.2bind
#include <sys/types.h>
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:绑定套接字和网络信息结构体参数:sockfd:socket函数产生的套接字文件描述符(socket函数返回值)
addr:避免冲突警告
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}上面的结构体只是为了强转防止编译警告的,实际使用的是下面的结构体
用IPV4看AF_INET的man pagestruct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};addrlen:addr的长度
返回值:成功:0
失败:-1 重置错误码
2.3listen
#include <sys/types.h>
#include <sys/socket.h>int listen(int sockfd, int backlog);产生半连接队列
功能:将套接字设置成被动监听状态,只有这个状态的套接字才能等待客户端连接参数:sockfd:socket函数返回值
backlog:半连接队列的长度,一般传 5 10 都行 不是0就行
返回值:成功:0
失败:-1 重置错误码
2.4accept
#include <sys/types.h>
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);产生全连接队列,返回值创 建的文件描述符
功能:提取半连接队列中的第一个客户端连接,成功,放到函数产生的全连接队列中,专门 用于和当前客户端通信,返回的套接字和原套接字类型相同参数:sockfd:listen后的sockfd
addr:客户端的网络信息结构体首地址,不关心写NULL
addrlen:客户端addr长度,不关心写NULL
返回值:成功:创建的新的文件描述符 专门用于和当前客户端通信
失败:-1 重置错误码
2.5connect
#include <sys/types.h>
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:与服务器建立连接参数:sockfd:accept函数返回值
addr:服务器的网络信息结构体
addrlen:addr的长度
返回值:成功:0
失败:-1 重置错误码
计算器网络通信代码实现
服务器代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
//1.创建套接字
int sockfd=0;//需要接函数返回值,后面函数会用
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
//2.服务器网络信息结构体填充
//struct sockaddr addr;
struct sockaddr_in ad;
memset(&ad, 0, sizeof(ad));
ad.sin_family=AF_INET;
ad.sin_port=htons(7788);//自己随意指定,不冲突就行,冲突就换
ad.sin_addr.s_addr=inet_addr("192.168.250.100");
//3.绑定套接字和服务器网络信息结构体
if(-1==bind(sockfd,(struct sockaddr*)&ad,sizeof(ad))){
ERRLOG("bind error");
}
//4.让套接字处于被动监听状态
if(-1==listen(sockfd,5)){
ERRLOG("listen error");
}
//5.阻塞等待客户端连接
printf("正在等待客户端发来信息\n");
int newfd=0;
if((newfd=accept(sockfd,NULL,NULL))==-1){
ERRLOG("accept error");
}
printf("客户端连接成功\n");
//6.收发数据
char buff[128]={0};
int lnum=0;
char opr=0;
int rnum=0;
int result=0;
read(newfd,buff,128);
printf("客户端发来数据:%s\n",buff);
sscanf(buff,"%d%c%d",&lnum,&opr,&rnum);
switch(opr){
case '+':
result=lnum+rnum;
break;
case '-':
result=lnum-rnum;
break;
case '*':
result=lnum*rnum;
break;
case '/':
result=(float)lnum/(float)rnum;
break;
}
memset(buff,0,128);
sprintf(buff,"result=%d",result);
write(newfd,buff,128);
//7.关闭套接字
close(sockfd);
close(newfd);
return 0;
}
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
//创建套接字
int sockfd=0;
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){
ERRLOG("socket error");
}
//填充服务器结构体
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_port=htons(6666);
addr.sin_addr.s_addr=inet_addr("192.168.2.84");
//连接服务器
if(connect(sockfd,(struct sockaddr *)&addr,sizeof(addr))==-1){
ERRLOG("connect error");
}
//收发数据
char buff[128]={0};
//写入的数据从终端获取
fgets(buff,128,stdin);
buff[strlen(buff)-1]='\0';
write(sockfd,buff,128);
memset(buff,0,128);
read(sockfd,buff,128);
printf("服务器发来的信息是:%s\n",buff);
//7.关闭套接字
close(sockfd);
return 0;
}
2.6 send
#include <sys/types.h>
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据参数:sockfd:accept函数返回值
buf:要发送的数据的缓冲区首地址
len:要发送数据的长度
flags:发送的标志位,0用法和write就一样了,MSG_DONTWAIT 表示非阻塞
返回值:成功:实际发送的字节数
失败:-1 重置错误码
注意:
如果对方关闭了套接字或者断开连接
第一次send没有反应,第二次send会产生SIGPIPE信号
下面三种用法是等价的:
write(sockfd, buf, 128);
send(sockfd, buf, 128, 0);
sendto(sockfd, buf, 128, 0, NULL, NULL);
2.7 recv
#include <sys/types.h>
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据参数:
sockfd:accept函数返回值
buf:要接收的数据的缓冲区首地址
len:要接收数据的长度
flags:发送的标志位,flags:接收的标志位,0用法和read就一样了, MSG_DONTWAIT 表示非阻塞
返回值:成功:实际接收的字节数
失败:-1 重置错误码
注意: 如果对方关闭了套接字或者断开连接 recv 会返回0
下面三种用法是等价的:
read(sockfd, buf, 128);
recv(sockfd, buf, 128, 0);
recvfrom(sockfd, buf, 128, 0, NULL, NULL);
五、桥接模式和NAT模式的区别
window的防火墙都关掉:
有些杀毒软件,如 火绒 迈克菲 等软件会禁止关闭防火墙
通信代码模板
服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
//入参合理性检查
if(argc!=3){
printf("Usgae: %s <ip> <port>",argv[0]);
}
//创建套接字
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in severaddr;
severaddr.sin_family=AF_INET;
severaddr.sin_port=htons(atoi(argv[2]));
severaddr.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t severaddrlen=sizeof(severaddr);
//绑定
if(-1==bind(sockfd,(struct sockaddr *)&severaddr,&severaddrlen)){
ERRLOG("bind error");
}
//监听
if(listen(sockfd,5)==-1){
ERRLOG("listen error");
}
//创建客户端信息
struct sockaddr_in clientaddr;
//clientaddr.sin_family=AF_INET;
memset(&clientaddr, 0, sizeof(clientaddr));//清空就行
socklen_t clientaddrlen=sizeof(clientaddr);
//阻塞等待连接
int acpfd=0;
char buff[128]={0};
while(1){
printf("等待客户端连接\n..");
if(-1==(acpfd=accept(sockfd,(struct sockaddr *)&clientaddr,&clientaddrlen))){
ERRLOG("accept error");
}
printf("客户端%s %d已连接\n..",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
//接收客户端发来的文件
if((recnum=recv(acpfd,buff,128,0))==-1){
ERRLOG("recv error");
}else if(recnum==0){
printf("客户端[%s:%d]断开连接了..\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
close(acpfd);
break;
}
if(srtcnmp(buff,"quit",4)){
printf("客户端[%s:%d]退出了",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
}
//收发数据
while(1){
close(acpfd);
}
}
//关闭文件
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
//入参合理性检查
if(argc!=3){
printf("Usgae: %s <ip> <port>",argv[0]);
}
//创建套接字
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in severaddr;
severaddr.sin_family=AF_INET;
severaddr.sin_port=htons(atoi(argv[2]));
severaddr.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t severaddrlen=sizeof(severaddr);
//连接
if(-1==connect(sockfd,(struct sockaddr *)&severaddr,&severaddrlen)){
ERRLOG("connect error");
}
printf("请发送数据\n");
char buff[128]={0};
while(1){
}
//关闭文件
close(sockfd);
return 0;
}
练习
练习1:
使用TCP客户端下载TCP服务器所在目录下的文件的功能
----TCP粘包问题
流程:
客户端给服务器发送要下载的文件名
服务器收到文件名之后,判断当前路径下有没有该文件
//open(O_RDONLY) 如果报错 且错误码为 ENOENT 说明文件不存在
如果不存在,服务器给客户端发送文件不存在的消息
如果存在,服务器也要先发送文件存在的消息给客户端
然后循环读取文件内存,发送给客户端
发完文件内容之后 再发送一个特殊的结束标志 "****OVER****"
客户端接到服务器的应答后,
如果文件不存在,则重新发送文件名给服务器
如果存在,open(O_WRONLY|O_CREAT|O_TRUNC,0664)
循环读取文件内容并写入文件
注意:服务器程序和客户端程序不要运行在同一个路径下。
下面的代码运行后,服务器没有什么问题,但是客户端会卡住,对比后发现,下载的文件内容也多了一行****OVER****,发现是tcp的粘包问题造成的,卡住这个现象的出现是正常的
服务器代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t serverlen=sizeof(server);
if(-1==bind(sockfd,(struct sockaddr *)&server,serverlen)){
ERRLOG("bind error");
}
if(-1==listen(sockfd,5)){
ERRLOG("listen error");
}
int acceptfd;
struct sockaddr_in client;
socklen_t clientlen=sizeof(client);
int ret=0;
char filename[32]={0};
char buf[128]={0};
int fd=0;
int readret=0;
while(1){
printf("等待客户端连接...\n");
if(-1==(acceptfd=accept(sockfd,(struct sockaddr *)&client,&clientlen))){
ERRLOG("accept error");
}
printf("客户端[%s:%d]已连接...\n",inet_ntoa(client.sin_addr),client.sin_port);
//接收文件名
FLAG:
if(-1==(ret=recv(acceptfd,filename,32,0))){
ERRLOG("recv error");
}else if(ret==0){
printf("客户端[%s:%d]断开连接..\n",inet_ntoa(client.sin_addr), ntohs(client.sin_port));
close(acceptfd);
continue;
}
printf("要下载的文件名是[%s]\n",filename);
//判断文件是否存在
if(-1==(fd=open(filename,O_RDONLY))){
if(errno==ENOENT){
//sprintf(buf,"file do not exist");
strcpy(buf,"file do not exist");
//文件状态发送给客户端
if(-1==send(acceptfd,buf,128,0)){
ERRLOG("send error");
}
goto FLAG;
}else{
//sprintf(buf,"open error");
strcpy(buf,"open error");
if(-1==send(acceptfd,buf,128,0)){
ERRLOG("send error");
}
}
}else{
//sprintf(buf,"file exist");
strcpy(buf,"file exist");
if(-1==send(acceptfd,buf,128,0)){
ERRLOG("send error");
}
}
//循环读取文件内容发给客户端
while(0<(readret=read(fd,buf,128))){
if(-1==send(acceptfd,buf,readret,0)){
ERRLOG("send error");
}
memset(buf,0,128);
}
//sprintf(buf,"****OVER****");
strcpy(buf,"****OVER****");
if(-1==send(acceptfd,buf,12,0)){
ERRLOG("send error");
}
close(acceptfd);
}
close(sockfd);
return 0;
}
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
//运行客户端代码会卡住,测试发现卡在recv的位置上了,出现这个现象的原因就是
//由于tcp的粘包问题产生的
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t serverlen=sizeof(server);
if(-1==connect(sockfd,(struct sockaddr *)&server,serverlen)){
ERRLOG("connect error");
}
printf("与服务器连接成功...\n");
char filename[32]={0};
char buf[128]={0};
int fd=0;
int ret=0;
//从终端获取文件名
FLAG:
printf("请输入要下载的文件名\n");
memset(filename,0,32);
memset(buf,0,128);
fgets(filename,32,stdin);
filename[strlen(filename)-1]='\0';
//发送文件名给服务器
if(-1==send(sockfd,filename,32,0)){
ERRLOG("send error");
}
//接收文件状态回复
if(-1==recv(sockfd,buf,128,0)){
ERRLOG("recv error");
}
//判断文件是否存在
if(strcmp(buf,"file do not exist")==0){
printf("[%s] do not exist,please input filename again\n",filename);
goto FLAG;
}
else if(strcmp(buf,"file exist")==0){
printf("[%s] exist\n",filename);
if(-1==(fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666))){
ERRLOG("open error");
}
}
//循环下载文件内容
while(1){
if(-1==(ret=recv(sockfd,buf,128,0))){
ERRLOG("recv error");
}
printf("2222\n");
if(!strcmp(buf,"****OVER****")){
break;
}
if(-1==write(fd,buf,ret)){
ERRLOG("write error");
}
}
printf("file download finished\n");
close(fd);
close(sockfd);
return 0;
}
粘包问题产生原因
tcp在传输的时候不是一调用send就直接将数据发送给客户端的,而是将数据放到发送缓冲区里
tcp底层的nagle算法,会将一定短时间内发送的数据包组装成一个整体,发送给对方
而接受方无法区分消息的边界和类型,就可能会导致有冲突的情况发生
也就是说,当文件只剩最后一部分的时候,很有可能将最后的结束字符和文件最后一部分作为一个整体发送给接收方,也有可能将三个128和最后的30+12一起发给接收方,但不管是哪种方式,接收方写入的时候是以128为单位接受的,所以接收方缓冲区里是无法单独比较over的,所以会产生粘包问题。
解决方法:
1.既然是一定短时间内的数据成为一个整体,那么最后发送的over就不和前面一块发送了,在over之前加一个延时函数,让他单独发送
但是不常用,因为服务器里禁止使用sleep
2.发送定长的数据包,
解决方法一代码
服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t serverlen=sizeof(server);
if(-1==bind(sockfd,(struct sockaddr *)&server,serverlen)){
ERRLOG("bind error");
}
if(-1==listen(sockfd,5)){
ERRLOG("listen error");
}
int acceptfd;
struct sockaddr_in client;
socklen_t clientlen=sizeof(client);
int ret=0;
char filename[32]={0};
char buf[128]={0};
int fd=0;
int readret=0;
while(1){
printf("等待客户端连接...\n");
if(-1==(acceptfd=accept(sockfd,(struct sockaddr *)&client,&clientlen))){
ERRLOG("accept error");
}
printf("客户端[%s:%d]已连接...\n",inet_ntoa(client.sin_addr),client.sin_port);
//接收文件名
FLAG:
if(-1==(ret=recv(acceptfd,filename,32,0))){
ERRLOG("recv error");
}else if(ret==0){
printf("客户端[%s:%d]断开连接..\n",inet_ntoa(client.sin_addr), ntohs(client.sin_port));
close(acceptfd);
continue;
}
printf("要下载的文件名是[%s]\n",filename);
//判断文件是否存在
if(-1==(fd=open(filename,O_RDONLY))){
if(errno==ENOENT){
//sprintf(buf,"file do not exist");
strcpy(buf,"file do not exist");
//文件状态发送给客户端
if(-1==send(acceptfd,buf,128,0)){
ERRLOG("send error");
}
goto FLAG;
}else{
sprintf(buf,"open error");
//strcpy(buf,"open error");
if(-1==send(acceptfd,buf,128,0)){
ERRLOG("send error");
}
}
}else{
sprintf(buf,"file exist");
//strcpy(buf,"file exist");
if(-1==send(acceptfd,buf,128,0)){
ERRLOG("send error");
}
}
//循环读取文件内容发给客户端
while(0<(readret=read(fd,buf,128))){
if(-1==send(acceptfd,buf,readret,0)){
ERRLOG("send error");
}
memset(buf,0,128);
}
#if 0
//解决方法1:将文件内容和结束标志间隔一定的时间发送 就可以解决
//但是服务器程序中 是禁止使用 sleep函数的
sleep(1);
#endif
sprintf(buf,"****OVER****");
//strcpy(buf,"****OVER****");
if(-1==send(acceptfd,buf,12,0)){
ERRLOG("send error");
}
close(acceptfd);
}
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
//运行客户端代码会卡住,测试发现卡在recv的位置上了,出现这个现象的原因就是
//由于tcp的粘包问题产生的
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t serverlen=sizeof(server);
if(-1==connect(sockfd,(struct sockaddr *)&server,serverlen)){
ERRLOG("connect error");
}
printf("与服务器连接成功...\n");
char filename[32]={0};
char buf[128]={0};
int fd=0;
int ret=0;
//从终端获取文件名
FLAG:
printf("请输入要下载的文件名\n");
memset(filename,0,32);
memset(buf,0,128);
fgets(filename,32,stdin);
filename[strlen(filename)-1]='\0';
//发送文件名给服务器
if(-1==send(sockfd,filename,32,0)){
ERRLOG("send error");
}
//接收文件状态回复
if(-1==recv(sockfd,buf,128,0)){
ERRLOG("recv error");
}
//判断文件是否存在
if(strcmp(buf,"file do not exist")==0){
printf("[%s] do not exist,please input filename again\n",filename);
goto FLAG;
}
else if(strcmp(buf,"file exist")==0){
printf("[%s] exist\n",filename);
if(-1==(fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666))){
ERRLOG("open error");
}
}
//循环下载文件内容
while(1){
memset(buf,0,128);//清空buf或者下面用strncmp
if(-1==(ret=recv(sockfd,buf,128,0))){
ERRLOG("recv error");
}
if(!strcmp(buf,"****OVER****")){
break;
}
if(-1==write(fd,buf,ret)){
ERRLOG("write error");
}
}
printf("file download finished\n");
close(fd);
close(sockfd);
return 0;
}
解决方法二代码
服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
//运行客户端代码会卡住,测试发现卡在recv的位置上了,出现这个现象的原因就是
//由于tcp的粘包问题产生的
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
//创建数据信息结构体
typedef struct msg{
int size;
char buf[128];
}msg_t;
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t serverlen=sizeof(server);
if(-1==connect(sockfd,(struct sockaddr *)&server,serverlen)){
ERRLOG("connect error");
}
printf("与服务器连接成功...\n");
char filename[32]={0};
msg_t msg;
char buf[128]={0};
int fd=0;
int ret=0;
//从终端获取文件名
FLAG:
printf("请输入要下载的文件名\n");
memset(filename,0,32);
memset(msg.buf,0,128);
fgets(filename,32,stdin);
filename[strlen(filename)-1]='\0';
//发送文件名给服务器
if(-1==send(sockfd,filename,32,0)){
ERRLOG("send error");
}
//接收文件状态回复
if(-1==recv(sockfd,&msg,sizeof(msg),0)){
ERRLOG("recv error");
}
//判断文件是否存在
if(strcmp(msg.buf,"file do not exist")==0){
printf("[%s] do not exist,please input filename again\n",filename);
goto FLAG;
}
else if(strcmp(msg.buf,"file exist")==0){
printf("[%s] exist\n",filename);
if(-1==(fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666))){
ERRLOG("open error");
}
}
//循环下载文件内容
while(1){
memset(&msg,0,sizeof(msg));//清空buf或者下面用strncmp
if(-1==(ret=recv(sockfd,&msg,sizeof(msg),0))){
ERRLOG("recv error");
}
if(msg.size==0){
break;
}
if(-1==write(fd,msg.buf,msg.size)){
ERRLOG("write error");
}
}
close(fd);
printf("file download finished\n");
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
//创建数据信息结构体
typedef struct msg{
int size;
char buf[128];
}msg_t;
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t serverlen=sizeof(server);
if(-1==bind(sockfd,(struct sockaddr *)&server,serverlen)){
ERRLOG("bind error");
}
if(-1==listen(sockfd,5)){
ERRLOG("listen error");
}
int acceptfd;
struct sockaddr_in client;
socklen_t clientlen=sizeof(client);
int ret=0;
char filename[32]={0};
char buf[128]={0};
msg_t msg;
int fd=0;
int readret=0;
while(1){
printf("等待客户端连接...\n");
if(-1==(acceptfd=accept(sockfd,(struct sockaddr *)&client,&clientlen))){
ERRLOG("accept error");
}
printf("客户端[%s:%d]已连接...\n",inet_ntoa(client.sin_addr),client.sin_port);
//接收文件名
FLAG:
if(-1==(ret=recv(acceptfd,filename,32,0))){
ERRLOG("recv error");
}else if(ret==0){
printf("客户端[%s:%d]断开连接..\n",inet_ntoa(client.sin_addr), ntohs(client.sin_port));
close(acceptfd);
continue;
}
printf("要下载的文件名是[%s]\n",filename);
//判断文件是否存在
if(-1==(fd=open(filename,O_RDONLY))){
if(errno==ENOENT){
//sprintf(buf,"file do not exist");
strcpy(msg.buf,"file do not exist");
//文件状态发送给客户端
if(-1==send(acceptfd,&msg,sizeof(msg),0)){
ERRLOG("send error");
}
goto FLAG;
}else{
sprintf(msg.buf,"open error");
//strcpy(buf,"open error");
if(-1==send(acceptfd,&msg,sizeof(msg),0)){
ERRLOG("send error");
}
}
}else{
sprintf(msg.buf,"file exist");
//strcpy(buf,"file exist");
if(-1==send(acceptfd,&msg,sizeof(msg),0)){
ERRLOG("send error");
}
}
//循环读取文件内容发给客户端
while(0<(readret=read(fd,buf,128))){
msg.size=readret;
strncpy(msg.buf,buf,readret);
if(-1==send(acceptfd,&msg,sizeof(msg),0)){
ERRLOG("send error");
}
memset(&msg,0,sizeof(msg));
}
msg.size=0;
if(-1==send(acceptfd,&msg,12,0)){
ERRLOG("send error");
}
close(fd);
close(acceptfd);
}
close(sockfd);
return 0;
}
练习2:
使用代码控制机械臂
机械臂程序使用QT写的一个基于windows运行的TCP服务器程序
运行在哪台主机,ip地址就是哪台主机的ip地址--windows的ip地址,
填写控制端口号之后,点击开启监听,服务器就运行起来了
windows 查看自己ip地址 ipconfig 即可
机械臂的协议
0xFF 0X02 (1) (2) 0XFF
(1) 表示要控制的摆臂
0x00 控制红色的摆臂 0x01 控制蓝色的摆臂
(2) 表示摆臂的偏移值
红 [-90, +90] 蓝 [0, +180]
要求:编写一个TCP客户端程序,在Ubuntu运行
通过 wsad 来控制机械臂的摆动
w 红减
s 红加
a 蓝减
d 蓝加
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
perror(msg);\
exit(-1);\
}while(0)
typedef struct blue{
unsigned char a;
unsigned char b;
unsigned char c;
unsigned char d;
unsigned char e;
}blue_t;
int main(int argc, char const *argv[])
{
//入参合理性检查
if(argc!=3){
printf("Usgae: %s <ip> <port>\n",argv[0]);
exit(-1);
}
//创建套接字
int sockfd=0;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in severaddr;
memset(&severaddr, 0, sizeof(severaddr));
severaddr.sin_family=AF_INET;
severaddr.sin_port=htons(atoi(argv[2]));
severaddr.sin_addr.s_addr=inet_addr(argv[1]);
socklen_t severaddrlen=sizeof(severaddr);
//连接
if(-1==connect(sockfd,(struct sockaddr *)&severaddr,severaddrlen)){
ERRLOG("connect error");
}
printf("lianjie chengg\n");
char redbuff[5]={0xFF,0x02,0,0,0xFF};
blue_t bluebuff={0xFF,0x02,1,0,0xFF};
char choose=0;
//先让机械臂回正
if(-1==send(sockfd,redbuff,5,0)){
ERRLOG("send error");
}
usleep(100000);//由于QT是基于信号槽实现的 对数据的响应不敏感 加一个小的时间间隔
if(-1==send(sockfd,&bluebuff,5,0)){
ERRLOG("send error");
}
printf("请发送数据\n");
while(1){
choose=getchar();
getchar();//消除回车干扰
switch(choose){
case 'w':
if(redbuff[3]>-0x55){
redbuff[3]-=5;
if(-1==send(sockfd,redbuff,5,0)){
ERRLOG("send error");
}
}else{
printf("到达极限\n");
}
break;
case 's':
if(redbuff[3]<0x55){
redbuff[3]+=5;
if(-1==send(sockfd,redbuff,5,0)){
ERRLOG("send error");
}
}else{
printf("到达极限\n");
}
break;
case 'a':
if(bluebuff.d>0){
bluebuff.d-=5;
if(-1==send(sockfd,&bluebuff,5,0)){
ERRLOG("send error");
}
}else{
printf("到达极限\n");
}
break;
case 'd':
if(bluebuff.d<0xAF){
bluebuff.d+=5;
if(-1==send(sockfd,&bluebuff,5,0)){
ERRLOG("send error");
}
}else{
printf("到达极限\n");
}
break;
}
if (choose=='q') {
break;
}
}
//关闭文件
close(sockfd);
return 0;
}
版本二
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
//创建套接字
int sockfd;
if(-1==(sockfd=socket(AF_INET,SOCK_STREAM,0))){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr=inet_addr(argv[1]);
serveraddr.sin_port=htons(atoi(argv[2]));
socklen_t serveraddrlen=sizeof(serveraddr);
//建立连接
if(-1==connect(sockfd,(struct sockaddr *)&serveraddr,serveraddrlen)){
ERRLOG("connect error");
}
printf("conncet success\n");
//收发数据
int red=0;
int blue=0;
char redbuf[5]={0xFF,0x02,0x00,0x00,0xFF};
char bluebuf[5]={0xFF,0x02,0x01,0x00,0xFF};
//先让机械臂归零
if(-1==send(sockfd,redbuf,sizeof(redbuf),0)){
ERRLOG("send error");
}
usleep(10000);
if(-1==send(sockfd,bluebuf,sizeof(bluebuf),0)){
ERRLOG("send error");
}
//从终端接收字符 w红- a红+ s蓝- d蓝+
char c=0;
while(1){
c=getchar();
getchar();//清理垃圾字符
if(c=='q'){
break;
}
switch (c)
{
case 'w':if(redbuf[3]>=-90 && redbuf[3]<=90){
red-=5;
*(redbuf+3)=red;
//redbuf[3]-=5;
if(-1==send(sockfd,redbuf,sizeof(redbuf),0)){
ERRLOG("send error");
}
}
break;
case 'a':if(redbuf[3]>=-90 && redbuf[3]<=90){
red+=5;
*(redbuf+3)=red;
//redbuf[3]+=5;
if(-1==send(sockfd,redbuf,sizeof(redbuf),0)){
ERRLOG("send error");
}
}
break;
case 's':if((bluebuf[3]>=0) && (bluebuf[3]<=180)){
//blue-=5;
//*(bluebuf+3)=blue;
bluebuf[3]-=5;
if(-1==send(sockfd,bluebuf,sizeof(bluebuf),0)){
ERRLOG("send error");
}
}
break;
case 'd':if((bluebuf[3]>=0) && (bluebuf[3]<=180)){
//blue+=5;
//*(bluebuf+3)=blue;
bluebuf[3]+=5;
if(-1==send(sockfd,bluebuf,sizeof(bluebuf),0)){
ERRLOG("send error");
}
}
break;
}
}
//关闭套接字
close(sockfd);
return 0;
}
六、UDP网络编程
1.流程
服务器流程:
创建用户数据报套接字
填充服务器的网络信息结构体
将套接字与服务器的网络信息结构体绑定
收发数据 recvfrom sendto
关闭套接字
客户端流程:
创建用户数据报套接字
填充服务器的网络信息结构体
收发数据 recvfrom sendto
关闭套接字
2.函数
2.1 recvfrom函数
#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);
功能:在套接字上接收一条消息
参数: 前4个参数和 recv 函数的参数一样
后2个参数和 accept 函数的后2个参数一样--用来保存发送方的网络信息结构体的
如果不关心可以传 NULL
返回值: 成功 实际接到的字节数
失败 -1 重置错误码
注意:recvfrom函数用于UDP中不会返回0
2.2 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);
功能:向套接字上发送一条消息
参数: 前4个参数和 send 函数的参数一样
后2个参数和 connect 函数的后2个参数一样--用来指定消息发给谁的
返回值: 成功 实际发送的字节数
失败 -1 重置错误码
3.实例
服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
//创建套接字
int sockfd;
if(-1==(sockfd=socket(AF_INET,SOCK_DGRAM,0))){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr=inet_addr(argv[1]);
serveraddr.sin_port=htons(atoi(argv[2]));
socklen_t serveraddrlen=sizeof(serveraddr);
//将套接字与服务器网络信息结构体绑定
if(-1==bind(sockfd,(struct sockaddr *)&serveraddr,serveraddrlen)){
ERRLOG("bind error");
}
//填充客户端网络信息结构体
struct sockaddr_in clientaddr;
socklen_t clientaddrlen=sizeof(clientaddr);
//收发数据
int ret=0;
char buf[128]={0};
while(1){
memset(buf,0,sizeof(buf));
if(-1==(ret=recvfrom(sockfd,buf,128,0,(struct sockaddr *)&clientaddr,&clientaddrlen))){
ERRLOG("recvfrom error");
}
printf("客户端[%s:%d]发来数据:[%s]\n",inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port),buf);
strcat(buf,"已收到");
if(strncmp(buf,"quit",4)==0){
break;
}
if(-1==sendto(sockfd,buf,128,0,(struct sockaddr *)&clientaddr,clientaddrlen)){
ERRLOG("sendto error");
}
}
//关闭套接字
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
#define ERRLOG(msg) do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc!=3){
printf("Usage:%s <ip> <port>\n",argv[0]);
exit(-1);
}
//创建套接字
int sockfd;
if(-1==(sockfd=socket(AF_INET,SOCK_DGRAM,0))){
ERRLOG("socket error");
}
//绑定服务器网络信息结构体
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr=inet_addr(argv[1]);
serveraddr.sin_port=htons(atoi(argv[2]));
socklen_t serveraddrlen=sizeof(serveraddr);
//收发数据
int ret=0;
char buf[128]={0};
while(1){
memset(buf,0,sizeof(buf));
fgets(buf,128,stdin);
buf[strlen(buf)-1]='\0';
if(-1==sendto(sockfd,buf,128,0,(struct sockaddr *)&serveraddr,serveraddrlen)){
ERRLOG("sendto error");
}
if(strncmp(buf,"quit",4)==0){
break;
}
if(-1==(ret=recvfrom(sockfd,buf,128,0,NULL,NULL))){
ERRLOG("recvfrom error");
}
printf("%s\n",buf);
}
//关闭套接字
printf("1111\n");
close(sockfd);
return 0;
}
为什么UDP可以实现并发,而TCP不行?
因为UDP在recvfrom处阻塞,只要有数据发送过来,就可以接收
而TCP有两次阻塞,他们相互影响,一次是accept,一次是recv,当进程阻塞在recv处时,即使有客户端加入也不能立即执行,因为阻塞说明进程在休眠,只有当recv退出以后才能执行到accept。
七、TFTP协议分析
1. TFTP概述
TFTP:简单文件传送协议
最初用于引导无盘系统,被设计用来传输小文件
特点:
基于UDP协议实现
不进行用户有效性认证
数据传输模式:
octet:二进制模式
netascii:文本模式
mail:已经不再支持
2. TFTP通信过程
3. TFTP通信过程总结(无选项)
1、服务器在69号端口等待客户端的请求
2、服务器若批准此请求,则使用临时端口与客户端进行通信
3、每个数据包的编号都有变化(从1开始)
4、每个数据包都要得到ACK的确认如果出现超时,则需要重新发送最后的包(数据或ACK)
5、数据的长度以512Byte传输
6、小于512Byte的数据意味着传输结束
4. TFTP协议分析
错误码:
0 :未定义,参见错误信息
1 :File not found.
2 :Access violation.
3 :Disk full or allocation exceeded.
4 :illegal TFTP operation.
5 :Unknown transfer ID.
6 :File already exists.
7 :No such user.
8 :Unsupported option(s) requested.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <errno.h>
#define ERRLOG(msg)do{\
printf("%s %s %d\n",__FILE__,__func__,__LINE__);\
perror(msg);\
exit(-1);\
}while(0)
int main(int argc, char const *argv[])
{
//传参合理性检查
if(argc!=3){
printf("USage:<%s> <ip> <port>\n",argv[0]);
exit(-1);
}
//创建套接字
int sockfd;
char buf[600]={0};
char filename[32]={0};
int ret=0;
int nbytes=0;
unsigned short code=0;
unsigned short num=0;
char txt[512]={0};
int fd=0;
int recv_count=0;//用来保存已经接收到的数据包的编号
if(-1==(sockfd=socket(AF_INET,SOCK_DGRAM,0))){
ERRLOG("socket error");
}
//服务器网络信息结构体
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
RENAME:
serveraddr.sin_addr.s_addr=inet_addr(argv[1]);
serveraddr.sin_port=htons(atoi(argv[2]));
socklen_t serveraddrlen=sizeof(serveraddr);
//组装请求消息
//组装方式1:通过改变指针的操作空间来处理
//*(unsigned short *)buff = htons(1);
//组装方式2:一个字节一个字节的处理
//buf[0]=0;
//buf[1]=1;//需注意网络字节序的问题
//组装方式3:使用sprintf处理
//获取文件名
//RENAME:不能放在这,因为输入文件名在发送请求里,发送请求需要发给69,后面recvfrom已经将69覆盖了
printf("请输入要下载的文件名:\n");
scanf("%s",filename);
//组装请求
nbytes=sprintf(buf,"%c%c%s%c%s%c",0,1,filename,0,"octet",0);//sprintf的返回值是组装的字节数
//发送请求
if(-1==sendto(sockfd,buf,nbytes,0,(struct sockaddr*)&serveraddr,serveraddrlen)){
ERRLOG("sendto error");
}
while(1){
memset(buf,0,sizeof(buf));
//接收数据包
//思考:此处是否需要保存服务器的网络信息结构体?
//需要,因为一开始是69,后面通信是新建的临时的端口号,端口号改变了
//可以新定义一个变量,也可以直接使用前面构建的结构体,会直接覆盖
if(-1==(ret=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&serveraddr,&serveraddrlen))){
ERRLOG("recvfrom error");
}
//解析数据包
code=ntohs(*(unsigned short *)buf);
num=ntohs(*(unsigned short *)(buf+2));
strncpy(txt,buf+4,ret-4);
if(code==3&&(num==recv_count+1)){
recv_count++;
//将文件写入本地
if(num==1){
if(-1==(fd=open(filename,O_CREAT|O_TRUNC|O_WRONLY,0666))){
ERRLOG("open error");
}
}
if(-1==write(fd,txt,ret-4)){
ERRLOG("write error");
}
//组装ACK
//sprintf(buf,"%c%c%c",0,4,num);因为num是short类型的,sprintf反倒不好组装了
*(unsigned short *)buf = htons(4);
*(unsigned short *)(buf+2) = htons(num);
//发送ACK
if(-1==sendto(sockfd,buf,4,0,(struct sockaddr*)&serveraddr,serveraddrlen)){
ERRLOG("sendto error");
}
if(ret-4<512){
break;
}
}else if(code==5){
//如果出错,打印错误码和错误信息
printf("[%d:%s]\n",num,txt);
if(num==1){
goto RENAME;
}else{
exit(-1);
}
}
}
printf("%s download finish.\n",filename);
close(fd);
close(sockfd);
return 0;
}
八、wireshark抓包分析
1.软件的运行
windows:
鼠标右键点击图标
以管理员身份运行
Ubuntu:
sudo wireshark
选择要捕获的网卡,双击即可
或者点击菜单栏的 捕获-->选项-->选择合适的网卡-->点击开始也可以
我自己选的是WLAN,不一定按照图里的选
工作区说明:
过滤器的使用:
常用的过滤语句:
tcp.port==8888
ip.src==192.168.2.82
ip.dst==192.168.2.70
ip.addr==192.168.2.50
也可以使用 and 或者 or 连接多句
2.抓包分析
window的网络调试助手作为TCP服务器
Ubuntu的代码作为TCP客户端,给服务器发送hello,进行抓包分析
2.1 链路层
以太网头(MAC头)
目的MAC地址:接受者的MAC地址,windows的MAC地址
源MAC地址:发送者的MAC地址,Ubuntu的MAC地址
类型:指定了后面使用的协议的类型
0800 IP协议
0806 ARP协议
8035 RARP协议
查看MAC地址的方法 Ubuntu 中 ifconfig即可 windows 中 ipconfig/all
注意:交换机是工作在链路层的设备,他是根据MAC地址决定消息如何转发的。
2.2 网络层
IP头
版本:IP协议的版本 4 表示 ipv4
首部长度:IP头的长度 4个bit位 能表示的最大数是15 而IP头最小也是20字节
所以此处使用的是 4倍的单位 (0101)5就表示20了
总长度:IP头+TCP头+用户数据的长度 我们的例子中:20+20+128 == 168
生存时间:TTL,表明是数据包在网络中的寿命,即为“跳数限制”,由发出数据包的源点设置这个 字段。
路由器在转发数据之前就把TTL值减一,当TTL值减为零时,就丢弃这个数据报。
通常设置为32、64、128
协议类型:指定了后面使用的协议的类型
常用的ICMP(1),IGMP(2),TCP(6),UDP(17)
源IP地址:发送方的IP地址 Ubuntu的IP地址
目的IP地址:接收方的IP地址 windows的IP地址
注意:路由器是工作在网络层的设备,他是根据IP地址决定如何转发消息的。
2.3 传输层
TCP头
源端口号:发送方的端口号 Ubutnu的 随机的(没有bind)
目的端口号:接收方的端口号 windows的 5678
序列号:seq
确认号:ack
头部长度:TCP头的长度 也是4倍的单位 5 表示 20字节
2.4 应用层
3.TCP的三次握手和四次挥手
5.1 三次握手
三次握手发生在建立的过程中,由客户端主动发起。
发生在客户端的connect函数和服务器的accept函数(listen函数)之间的。
三次握手的过程是为了保证通信的双方都知道对方的收发数据的能力没问题。
同时,也是同步序列号的过程。
1.5.2 四次挥手
四次挥手是由主动关闭方发起的(一般情况下,由客户端发起居多)
四次挥手发生在连接断开的过程中
以客户端发起为例
当ACK置一的时候,只看确认号,不看序列号
这几位在数据包发送的时候,置一用大写的标志位表示,再详细数据里可以看到对应的位被置一
同步序列号的过程
九、TCP/UDP网络编程总结
1、TCP网络编程
1.客户端一般不需要绑定自己的网络信息结构体
因为操作系统会自动给客户端的ip地址和端口号赋值,也方便用户操作。
如果想要手动指定,也可以,需要调用bind()函数即可。
2.服务器端accept函数的后两个参数即使设置成NULL,服务器也可以给客户端回复消息,
原因是,服务器侧不是依赖于手动给定的IP地址和端口号来联系客户端的,而是给每个
客户端都分配一个独立的文件描述符 acceptfd,来专门用于和该客户端通信,
也就是说TCP的服务器,acceptfd和客户端是一一对应的关系。
3.TCP网络编程中可以使用 read/write send/recv sendto/recvfrom 来收发数据。
4.服务器端的accept函数本质就是一个阻塞的读函数,也就是一个接收函数,
客户端的connect函数本质就是个写函数,也就是一个发送函数,
收发的数据本质就是客户端的网络信息结构体。
5.TCP的服务器默认的是一个循环服务器,没法同时处理多个客户端的请求,
原因是tcp的服务器有两个阻塞函数 accept 和 recv,两个函数之间相互会有影响。
可以使用 多进程 多线程 IO多路复用 来解决。
2、UDP网络编程
1.UDP是无连接的,但是也可以双向的收发数据
因为UDP使用的是 sendto/recvfrom 来收发数据,sendto时可以指定接收方的信息。
sendto函数相当于 send函数和 connect函数的二合一
recvfrom函数相当于 recv函数和 accept函数的二合一
2.UDP中客户端也可以使用 connect 函数先将服务器的信息缓存在自己的内核中,
然后就可以使用 send 和 recv 收发数据了。
3.如果UDP服务器端的recvfrom函数的后两个参数设置成 NULL了,那么接收数据是没有问题的,
但是就没法给发送方回信了,因为sendto的后两个参数没法填写。
4.UDP服务器默认的就是一个并发服务器,因为只有一个阻塞的函数 recvfrom。
十、IO模型
1.分类
在UNIX/Linux下主要有4种I/O 模型:
阻塞I/O:
最常用、最简单、效率最低
非阻塞I/O:
可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:
允许同时对多个I/O进行控制
信号驱动I/O:
一种异步通信模型--后面驱动课程讲
2.阻塞IO
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
缺省情况下,套接字建立后所处于的模式就是阻塞I/O 模式。
前面学习的很多读写函数在调用过程中会发生阻塞。
读操作中的 read、recv、recvfrom
写操作中的 write、send
其他操作 accept、connect
写操作也是有阻塞的,如管道写满了,进程就会阻塞,
等待有足够的空间容纳下本次的写操作了,写操作继续执行
只不过,我们大多数情况下更关注 读阻塞 的问题。
以读阻塞为例:
当进程执行到读操作的时候,如果缓冲区中有内容,则读取内容继续向下执行
如果缓冲区中没有内容,进程就会进入休眠态,直到缓冲区中有内容了,由内核
唤醒该进程,来读走缓冲区的内容,然后继续向下执行。
以管道为例,演示读阻塞:
读端:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,const char * argv[])
{
int fd1 = open("fifo1", O_RDONLY);
int fd2 = open("fifo2", O_RDONLY);
int fd3 = open("fifo3", O_RDONLY);
char buff[128];
while(1){
memset(buff, 0, 128);
read(fd1, buff, 128);
printf("fifo1:[%s]\n", buff);
memset(buff, 0, 128);
read(fd2, buff, 128);
printf("fifo2:[%s]\n", buff);
memset(buff, 0, 128);
read(fd3, buff, 128);
printf("fifo3:[%s]\n", buff);
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
三个写端一样,只不过操作的管道文件不一样:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc,const char * argv[])
{
int fd = open("fifo1", O_WRONLY);
char buff[128];
while(1){
memset(buff, 0, 128);
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
write(fd, buff, 128);
}
close(fd);
return 0;
}
3 .非阻塞IO
以读操作为例:
当进程执行到读操作的时候,如果缓冲区中有内容,则读取内容继续向下执行
如果缓冲区中没有内容,会立即返回一个错误,而不是让进程进入休眠态,
但是,这种操作,需要配合一个循环来不停的测试是否有数据可读,
这种操作是十分浪费CPU的,一般不推荐使用。
有一部分函数,自身就带有非阻塞的标志位
如:waitpid 的 WHOHANG
recv 和 recvfrom 的 MSG_DONTWAIT O_NONBLOCK
但是大部分的函数是没有非阻塞标志位的,这时就可以使用 fcntl 函数来设置非阻塞。
fcntl 函数说明
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能: 控制文件描述符的状态
参数: fd:要操作的文件描述符
cmd:控制的命令
F_GETFL 获取文件描述符的状态,arg被忽略
F_SETFL 设置文件描述符的状态,arg是一个int类型
其中 O_NONBLOCK 表示非阻塞的标志位
...:可变参
返回值: 如果cmd为F_GETFL 成功返回文件状态标志位
如果cmd为F_SETFL 成功 返回0
失败 -1 重置错误码
使用fcntl设置非阻塞:
int flag = fcntl(fd, F_GETFL);//先获取原有的状态
flag |= O_NONBLOCK; //添加非阻塞的标志位
fcntl(fd, F_SETFL, flag);//再重新设置回去
例:
读端:
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(int argc,const char * argv[])
{
int fd1 = open("fifo1", O_RDONLY);
int fd2 = open("fifo2", O_RDONLY);
int fd3 = open("fifo3", O_RDONLY);
//设置非阻塞
int flag = fcntl(fd1, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd1, F_SETFL, flag);
flag = fcntl(fd2, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd2, F_SETFL, flag);
flag = fcntl(fd3, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd3, F_SETFL, flag);
char buff[128];
while(1){
memset(buff, 0, 128);
read(fd1, buff, 128);
printf("fifo1:[%s]\n", buff);
memset(buff, 0, 128);
read(fd2, buff, 128);
printf("fifo2:[%s]\n", buff);
memset(buff, 0, 128);
read(fd3, buff, 128);
printf("fifo3:[%s]\n", buff);
//sleep(1);//此处的sleep(1)是为了防止刷屏 看现象的
//如果把sleep(1)去掉 可以使用 top 看到 CPU基本已经被占满了
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
写端和前面的一样
4.多路IO复用
基本思想:
构造一个关于文件描述符的表,将所有要监视的文件描述符都放在这个表里,
将这个表传给一个函数(select poll epoll),这个函数本身默认也是阻塞的
当监视的文件描述符的表中有一个或多个文件描述符准备就绪的时候,这个函数会立即返回
返回时会告诉我们哪些文件描述符已经就绪了,我们已经知道哪些就绪了
再去执行对应的IO操作,就不会阻塞了。
select函数说明:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能: 实现多路IO复用
参数: nfds: 要监视的最大的文件描述符+1
readfds: 要监视的读文件描述符集合 不关心 可以传NULL
writefds: 要监视的写文件描述符集合 不关心 可以传NULL
exceptfds:要监视的异常文件描述符集合 不关心 可以传NULL
timeout: 超时时间 如果为NULL 则select永久阻塞 直到有就绪的文件描述符
返回值: 成功 就绪的文件描述符的个数
超时 0
失败 -1 重置错误码
void FD_CLR(int fd, fd_set *set);//将文件描述符在集合中删除
int FD_ISSET(int fd, fd_set *set);//判断文件文件描述符是否在集合中
// 在 返回非0 不在 返回0
void FD_SET(int fd, fd_set *set);//将文件描述符添加到集合中
void FD_ZERO(fd_set *set);//清空集合
注意事项:
1.select只能用于监视小于FD_SETSIZE(1024)的文件描述
2.select每次返回时会将没有就绪的文件描述符在集合中擦除
所以,在循环中使用时,每次需要重置集合
3.我们一般只监视读文件描述符集合
使用实例:
读端
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
//创建有名管道mkfifo,在终端直接用命令创建mkfifo fifo1 fifo2 fifo3
int fd1=open("fifo1",O_RDONLY);
int fd2=open("fifo2",O_RDONLY);
int fd3=open("fifo3",O_RDONLY);
char buf[128]={0};
//建立文件描述符监视表
fd_set readfds;//用来每次重新赋值的母本
FD_ZERO(&readfds);
fd_set readfds2;//副本
FD_ZERO(&readfds2);
//确定最大的文件描述符
int max_fd=0;
//将文件描述符添加到集合中
FD_SET(fd1,&readfds);
max_fd=max_fd >fd1 ? max_fd : fd1;
FD_SET(fd2,&readfds);
max_fd=max_fd >fd2 ? max_fd : fd2;
FD_SET(fd3,&readfds);
max_fd=max_fd >fd3 ? max_fd : fd3;
//调用select函数监视
int ret=0;
while(1){
//调用select监视文件描述符
readfds2=readfds;
if(-1==(ret=select(max_fd+1,&readfds2,NULL,NULL,NULL))){
perror("select error");
exit(-1);
}
#if 0
//判断文件描述符是否就绪
FD_ISSET(fd1,&readfds2);
memset(buf,0,128);
read(fd1,buf,128);
printf("%s\n",buf);
FD_ISSET(fd2,&readfds2);
memset(buf,0,128);
read(fd2,buf,128);
printf("%s\n",buf);
FD_ISSET(fd3,&readfds2);
memset(buf,0,128);
read(fd3,buf,128);
printf("%s\n",buf);
#endif
//循环判断
for(int i=fd1;i<max_fd+1&&ret!=0;i++){
if(FD_ISSET(i,&readfds2)){
memset(buf,0,128);
read(i,buf,128);
printf("[%s]\n",buf);
ret--;
}
}
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
写端(记得换管道名):
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
int fd=open("fifo1",O_WRONLY);
if(-1==fd){
perror("open error");
exit(-1);
}
char buf[128]={0};
while(1){
memset(buf, 0, 128);
fgets(buf,128,stdin);
buf[strlen(buf)-1]='\0';
write(fd,buf,128);
}
close(fd);
return 0;
}
对于select的一些理解
1.集合fd_set的本质?
ctrl过去发现只有第62行是被使用的,下面着重分析这一行代码
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
__fd_mask是long类型的别名
__FD_SETSIZE是1024
__NFDBITS]是(8 * (int) sizeof (__fd_mask)),也就是8*8=64
那么这行代码可以转换成 long __fds_bits[16];
也就是说fd_set的本质是结构体存放了一个long类型的,数组名为 __fds_bits,大小是16的数组
为什么不直接用数组,要把数组放到结构体里呢?
在写代码的时候,需要用母本给副本赋值,用结构体可以直接用等号赋值,而数组不行。
2.为什么只能监视小于FD_SETSIZE(1024)
上一个问题里的数组 __fds_bits[16]时将每一个比特位用来监视文件描述符的,也就是有
16*8*8=1024个比特位,所以只能监测0-1023个文件描述符,1024就越界了。
之所以采用这种比特数组的方式,是因为在监测文件描述符时,想要监测哪个文件描述
符就将哪个文件描述符置一,没有就绪就将文件描述符置零,这样如果用一个字节来操作的
话浪费空间,所以采用一个比特位存放一个文件描述符的状态的用法。
3.四个宏的本质
系统封装好的方便操作的位运算
void FD_CLR(int fd, fd_set *set);//删除文件描述符(清零)
int FD_ISSET(int fd, fd_set *set);//判断某一位是否是一
void FD_SET(int fd, fd_set *set);//添加文件描述符(置一)
void FD_ZERO(fd_set *set);//清空整个数组
4.为什么nfds要传max_fd+1?
因为监视的时候没有必要将数组里的0-1023全部监视一遍,只需要到最大的文件描述符即可。代码的逻辑是个for循环
for(int i=0;i<nfds;i++),如果是<=的话就是max_fd就行,<就需要是max_fd+1。
5.select返回值含义是什么,有什么用,怎么用?
就绪的文件描述符的个数。可以用来提升代码效率,当就绪的文件描述符不是最后一个的时候,可以不用将每个文件描述符都遍历一遍
用法:
//循环判断
for(int i=fd1;i<max_fd+1&&ret!=0;i++){
if(FD_ISSET(i,&readfds2)){
memset(buf,0,128);
read(i,buf,128);
printf("[%s]\n",buf);
ret--;
}
}
6.循环中使用select为什么每次要重置集合?
因为select函数会将没有就绪的文件描述符删除,但是下一次还是要继续监测这几个文件描述符,需要将母本的值拷贝给副本,来确保监视的文件描述符还是之前的。
十一、基于UDP的网络群聊聊天室
有新用户登录,其他在线的用户可以收到登录信息
有用户群聊,其他在线的用户可以收到群聊信息
有用户退出,其他在线的用户可以收到退出信息
服务器可以发送系统信息
提示:
客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程
服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程
服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存
数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据
流程图