文章目录
一、网络基础
- 多进程之间的通信通过内核,而多机通信需要使用网络
- 数据传输:协议,即数据格式
- 每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由
TCP/UDP对比
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前,不需 要建立连接
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
- 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
- TCP首部开销20字节;UDP的首部开销小,只有8个字节
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
UDP应用
- DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等
- 一般语音和视频通话都是使用udp来通信的
TCP/IP协议族体系
TCP/IP协议族体系是Internet事实上的工业标准。
一共有四层
应用层 | Relnet,FTP,HTTP,DNS,SMTP等 |
---|---|
传输层 | TCP和UDP |
网络层 | IP,ICMP和IGMP,端到端传输 |
网络接口和物理层 | 以太网,令牌环网,FDDI,wifi,gps/2G/3G/4G,驱动(屏蔽硬件差异) |
socket
-
是一个编程接口,是一个特殊的文件描述符(对他执行IO的操作函数,比如read,write,close等),并不仅限于TCP/IP协议,面向连接TCP,无连接UDP。
-
socket代表网络编程的一种资源
-
sock与HTTP和TCP之间的关系可以看:计算机网络——SOCKET、TCP、HTTP之间的区别与联系
分类:
- 流式套接字(SOCK_STREAM)。唯一对应 TCP 提供了一个面向连接,可靠的数据传输服务,数据无差错,无重复的发送顺序接收。内射击流量控制,避免数据流淹没慢的接收方。数据被看作式字节流,无长度限制。
- 数据包套接字(SOCK_DGRAM)。唯一对应UDP提供无连接服务器,数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
- 原始套接字(SOCK_RAW)。对应多个协议,发送穿透了传输层可以对较低层次协议(网络层)如IP,ICMP直接访问【跳过传输层】。
IP地址
-
IP地址是Internet中主机的标识,Internet中的主机要与别的机器通信必须具有一个IP地址,IP地址为32为(Ipv4)或者128位(Ipv6),每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由
-
表示的形式:常用点分十进制形式,如202.38.64.10,最后都会转化成一个32位的无符号整数
-
mobileIPV6:local IP(本地注册的IP),roma IP(漫游IP)
-
特殊的IP地址:
- 局域网IP:192.xxx.xxx.xxx或10.xxx.xxx.xxx
- 广播IP:xxx.xxx.xxx.255 255.255.255.255(全网广播)网络风暴
- 组播IP:224.xxx.xxx.xxx ~ 239.xxx.xxx.xxx
IP地址转化API
作用:将字符串转化为整型或整型转化为字符串
inet_addr()
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
- 返回转化后的整型数据
- 将strptr所指向的字符串转化为32位的网络字节序二进制
- 用在bind、send(TCP)、sendto(UDP)等发送报文的接口前
- 可以用在linux平台、windows平台,但是只支持ipv4地址,不支持ipv6地址
- 局限性:不能用于255.255.255.255的转化
inet_aton()
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char* straddr,struct in_addr *addrp);
- 将strptr所指向的字符串转化为32位的网络字节序二进制
inet_ntoa()
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
- 将一个32位网络字节序二进制地址转换为点分十进制的字符串
char* inet_ntoa(struct in_addr inaddr(AF_INET,); //把网络格式的ip地址转为字符串形式
- 用在recv(TCP)、recvfrom(UDP)等接收报文的接口前面
上面三个函数只能用在ipv4地址的转换
inet_pton() /inet_ntop()
人为识别的ip地址是点分十进制的字符串形式,但是计算机或者网络中识别的ip地址是整形数据,所以需要转化
#include <arpa/inet.h>
inet_pton(int af, const char *src, void *dst)
功能
- 转换字符串到网络地址(整型数据)
参数
- 第一个参数af是地址族、协议族(AF_INET或AF_INET6),
- 第二个参数*src是来源地址,是个字符串
- 第三个参数*dst接收转换后的数据存放的地址;是个整型数据
- 将IPV4/IPV6的地址转化为binary格式;能够处理255.255.255.255的转化
返回值:
- 成功返回1,失败返回其他
#include <arpa/inet.h>
inet_ntop(int af, const void *src, char *dst, socklen_t size);
功能:
- 将32位无符号整数转换成点分十进制数串
参数:
- 参数的作用和inet_pton相同,只是多了一个参数socklen_t size,
他是所指向缓存区dst的大小
,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将errno置为ENOSPC。 - size的宏定义
- #define INET_ADDRSTRLEN 16 //for ipv4
- #define INET6_ADDRSTRLEN 46 //for ipv6
返回值:
- 成功:则返回字符串的首地址
- 失败:返回NULL
案例:
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
char ip_str[]="192.168.3.103";
unsigned int ip_int = 0;
unsigned char *ip_p = NULL;
//将点分十进制ip地址转化为32位无符号整形数据
inet_pton(AF_INET,ip_str,&ip_int);
printf("ip_int = %d\n",ip_int);
ip_p = (char *)&ip_int;
printf("in_uint = %d,%d,%d,%d\n",*ip_p,*(ip_p+1),*(ip_p+2),*(ip_p+3));
return 0;
}
执行结果:
案例:
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
unsigned char ip_int[]={192, 168, 3, 103};
char ip_str[16] = ""; //"192.168.3.103"
inet_ntop(AF_INET, &ip_int, ip_str, 16);
printf("ip_s = %s\n", ip_str);
return 0;
}
执行结果:
端口号作用
- 一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等。这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。
- 实际上是通过“IP地址+端口号”来区 分不同的服务的。端口提供了一种访问通道,服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69。
- 16位数字,1-65535,
- 为了区分一台主机接收到的数据包应该转交给哪个任务进程处理,使用端口号来区别
- 预留端口,1-1023(FTP:24, SSH:22, HTTP: 80 ,HTTPS :469)
- 保留端口:1024-5000(不建议使用)
- 可以使用的端口: 5000~65535
- TCP端口号于UDP端口号独立
- 网络里的通信是由 IP地址+端口号 来决定的
端口号转化字节序API
- 端口号需要传递到网络上,需要使用api将主机字节序转化为网络字节序
- 当服务器打印客户端端口号时,需要使用api将网络字节序转化为主机字节序
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue); //主机字节序到网络字节序
uint16_t ntohs(uint16_t host16bitvalue); //网络字节序到主机字节序
字节序
定义:字节序是指多字节数据在计算机内存中存储或网络传输时各字节的存储顺序
常见的字节序:
- Little endian 小端字节序【将低序字节存储在起始地址】
- Big endian 大端字节序 【将高序字节存储在起始地址】
字节序是指不同的CPU访问内存中的多字节数据时候,存在大小端的问题
- 如果CPU访问的是字符串,则不存在大小端问题
- 把给定系统所采用的字节序称为主机字节序,为了避免不同类别主机之间在数据交换时由于对字节序的不同而导致差错,引入网络字节序
- 网络字节序=大端字节序【网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian(大端)排序方式】
- x86/ARM系列的CPU采用的都是小端字节序【不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机字节顺序】
- power/miop:arm作为路由时,采用的是大端模式
- 运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
- 异构计算机之间通讯【无论本机是大端还是小端】,需要转换自己的字节序为网络字节序
字节序转换api
#include <arpa/inet.h>
uint16_t htons(uint16_t host16bitvalue); //主机字节序到网络字节序
uint32_t htonl(uint32_t host32bitvalue); //主机字节序到网络字节序
uint16_t ntohs(uint16_t net16bitvalue); //网络字节序到主机字节序
uint32_t ntohl(uint32_t net32bitvalue); //网络字节序到主机字节序
h代表host,n代表net,s代表short(两个字节),l代表long(4个字节),
如何判断当前系统的字节序:用共用体
#include <stdio.h>
//判断当前系统的字节序
union un
{
int a;
char b;
};
int main(int argc, char const *argv[])
{
union un myun;
myun.a = 0x12345678;
printf("a = %#x\n", myun.a);
printf("b = %#x\n", myun.b);
if(myun.b == 0x78)
{
printf("小端存储\n");
}
else
{
printf("大端存储\n");
}
return 0;
}
二、TCP 编程 API
首先了解 计算机网络——SOCKET、TCP、HTTP之间的区别与联系
服务端开发步骤
- 创建套接字:socket()返回值是套接字的描述符
- 为套接字添加信息(IP地址和端口号):bind()
- 监听网络连接:listen()
- 监听到有客户端接入,接收一个连接:accept()
- 数据交互:read()和write()【也可以使用 recv()/send()】
- 关闭套接字,断开连接:close()
服务端开发函数
1. 创建套接字:
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
返回值:
- 成功则返回文件描述符
- 失败则返回-1
注意:
- 如果是IPV6编程,要使用struct sockddr_in6结构体(man 7 IPV6),通常使用struct sockaddr_storage来编程。
- 创建套接字时,系统不会分配端口,必须通过bind函数分配端口
- 创建的套接字默认属性是主动的,即主动发起服务的请求;当作为服务器时,往往需要
修改为被动的
2. 为套接字添加信息
函数原型:
#include <netinet/in.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
参数:
- addrlen:第三个参数为结构体的长度,使用sizeof(struct sockaddr_in)
注意:
-
第二个参数需要强制类型装换为(struct sockaddr*),因为我们使用的addr指针变量是由结构体struct sockaddr_in 创建的(替换了sockaddr结构体),因为不同场合所使用的结构体不一样,但是调用的函数却是同一个,所以定义一个通用结构体,当在指定场合使用时,在根据要求传入指定的结构体即可
-
使用sockaddr_in结构体替换sockaddr结构体:
//ipv4对应的是
struct sockaddr
{
sa_family_t sa_family; // 2字节,ip协议族
char sa_data[14] //14字节,ip+端口
};
同等替换为
struct sockaddr_in
{
sa_family_t sin_family;//协议族 2字节
in_port_t sin_port;//端口号 2字节
struct in_addr sin_addr;//ip地址结构体 4字节
unsigned char sin_zero[8]//填充,不起什么作用只是为了跟sockaddr结构体在内存对齐,这样才能相互转换 8字节
};
//ip地址结构体
struct in_addr{
uint32_t s_addr;//ip地址 4字节
}
-
TCP连接协议族选:AF_INET
-
第三个参数为IP地址结构体,配置结构体变量in_addr的成员s_addr
bind函数使用实例:
struct sokeaddr_in s_addr;//定义一个由sokeaddr_in创建的对象s_addr
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8989);//端口号需要转化为网络字节序
//IP地址转化的4种方式
//第一种:
inet_aton("127.0.0.1",&s_addr.sin_addr)//由上面定义的inet_aton第二个参数结构体指针,指向in_addr 结构体,所以取到该结构体,然后取其地址值
//第二种:
s_addr.sin_addr.s_addr = inet_addr("127.0.0.1")//char *inet_ntoa(struct in_addr in);
//第三种:
inet_pton(AF_INET,"127.0.0.1",(void *)s_addr.sin_addr.s_addr); //成功返回值为1
//第四种:应用在服务端,自动获取服务器IP地址,
s_addr.sin_addr.s_addr = INADDR_ANY;
3. 监听:
将套接字由主动设置为被动监听状态,使操作系统为该套接字设置一个连接队列(backlog表示连接队列长度),用来记录所有连接到该套接字的连接
函数原型:
int listen(int sockfd,int backlog);
参数:
返回值:
- 成功返回0
- 失败返回-1
注意:
- 第二个参数一般填写5
- 内核中服务器的套接字fd会维护2个链表
- 正在三次握手的客户端链表(数量=2*backlog+1)
- 已经建立好连接的客户端链表(已经完成三次握手分配好了的newfd)
4. 连接:
- 阻塞等待客户端连接,即队列里面没有完成握手的客户端则会阻塞等待
- 当服务器开启监听,内核会维护两个队列,此时accept会查询队列中完成三次握手的客户机,若有则与其连接,返回新的套接字描述符
函数原型:
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
返回值:
- 是新的套接字描述符,这个新的文件描述符用于后续与指定客户端读写操作,因为原先的套接字描述符可能被多个客户申请连接使用
参数与功能:
注意:
- 第二三个参数是客户端的套接字地址结构信息,如果不需要获取则可以为NULL。
- 如果accept第二三个参数都有,则可以用ip地址转换api打印客户机的ip地址或端口号
代码实例:
int newfd = -1;
struct sockaddr_in c_addr;
socklen_t addrlen = sizeof(c_addr);
newfd = accept(fd,(struct sockaddr *)&c_addr,&addrlen );
if(newfd <0){
perror("accept");
exit(1);
}
char ipv4_addr[16];
id(!inet_ntop(AF_INET,(void *)&c_addr,sin_addr,ipv4_addr,sizeof(c_addr))){
perror("inet_ntop");
exit(1);
}
printf("Client:(%s,%d)is connect!\n",ipv4_addr,ntohs(c_addr.sin_port));
- 第二个参数与bind函数一样,所以需要使用memset函数进行数据清空
#include <string.h>
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
- 第二个参数也需要强制类型装换为(struct sockaddr*)
5. 数据交互第一套API
函数原型:
ssize_t write(int fd,const void *buf,size_t nbytes);
ssize_t read(int fd,void *buf,size_t nbyte);
//函数返回值为读写的字节个数,错误则返回-1
//返回0表示客户端退出
注意:
-
此时的fd为accept的返回值,而不是socket()返回值
-
在套接字通信中进行字节读取函数与IO读取的略语有区别,因为他们输入或输出的字节数比请求的少
-
网络I/O操作:(一)read()/write()(二)recv()/send()(三)readv()/writev()(四)recvmsg()/sendmsg()(五)recvfrom()/sendto()
5. 数据交互的第二套API
- 在TCP套接字上发送数据函数:有连接
ssize_t send(int s,const void *msg,size_t len,int flags);
参数:
- sockfd:socket函数返回的fd
- buffer:发送缓冲区首地址
- length:发送的字节
- flags:发送方式(通常为0),作用和write一样
- MSG_DONTWAIT,非阻塞
- MSG_OOB:用于TCP类型的带外数据(out of band)
返回值:
- 成功:实际发送的字节数
- 失败:-1,并设置errno
- 在TCP套接字上接收数据函数:有连接
ssize_t recv(int s,void *buf,size_t len,int flags);
flag:一般填0,和read作用一样
特殊的标志:
- MSG_DONTWAIT
- MSG_OOB:读取带外数据
- MSG_PEEK:流
客户端开发步骤
1、创建套接字 socket()
2、发送客户端连接请求 connect()
3、进行通信 send()/recv()
4、关闭套接字 close()
客户端开发函数
1. 创建套接字
同服务端开发函数一样
2. 连接
函数原型:
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
参数:
注意:
- 头文件中<linux/in.h> 会与 <netinet/in.h>冲突
- connect和accept都会阻塞
- connect建立连接之后不会产生新的套接字,用的是socket的返回值
- 连接成功后才可以开始传输TCP数据,TCP连接的体现
- 头文件:#include <sys/socket.h>
3、close
1、使用 close 函数即可关闭套接字
- 关闭一个代表已连接套接字都将导致另一端接收到一个 0 长度的数据包(recv返回0)
2、做服务器时(有两个文件描述符)
- 1>关闭监听套接字将导致服务器无法接收新的连接,但不会影响已经建立的连接
- -2>关闭 accept 返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听
3、做客户端时(只有一个文件描述符)
- 关闭连接就是关闭连接,不意味着其他
三、TCP并发服务器
TCP原本不是并发服务器,TCP服务器同一时间只能与一个客户端通信
TCP不能实现并发的原因:
由于TCP服务器端有两个读阻塞函数,accept和recv,两个函数需要先后运行
,所以导致运行一个函数的时候另一个函数无法执行,无法保证一边连接客户端,一边与其他客户端通信,而udp原本就是并发服务器
1.TCP并发服务器多线程
server.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
void *client_data_hadler(void* arg);
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int Ret;
pthread_t tid;
int mark = 0;
char msg[128] = {0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if (argc != 3)
{
printf("param is not good\n");
exit(-1);
}
memset(&s_addr, 0, sizeof(struct sockaddr_in));
memset(&c_addr, 0, sizeof(struct sockaddr_in));
// 1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if (s_fd == -1)
{
perror("socket");
exit(-1);
}
// 2. bind 配置 struct sockaddr_in 结构体,绑定时再转换成 struct sockaddr * 结构体类型
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &s_addr.sin_addr);
Ret = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
if (Ret == -1)
{
perror("bind");
exit(-1);
}
// 3. listen
Ret = listen(s_fd, 10);
if (Ret == -1)
{
perror("listen");
exit(-1);
}
// 4. accept
int clen = sizeof(struct sockaddr_in);
while (1)
{
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if (c_fd == -1)
{
perror("accept");
break;
}
mark++;
printf("get connect: %s\n", inet_ntoa(c_addr.sin_addr));
sprintf(msg, "welcom No.%d client", mark); // 字符串拼接
write(c_fd, msg, strlen(msg));
pthread_create(&tid,NULL,client_data_hadler,(void*)&c_fd);
}
close(s_fd);
return 0;
}
//线程处理函数
void *client_data_hadler(void* arg){
int c_fd = *(int *)arg;
int n_read;
char readBuf[128];
//在服务端打印线程的id值
printf("thread c_fd= %d\n",c_fd);
while (1)
{
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if (n_read == -1)
{
perror("read");
}
else if (n_read > 0)
{
printf("\nget command: %s \n", readBuf);
}
else
{
printf("client quit\n");
break;
}
}
close(c_fd);
}
client.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int c_fd;
int n_read;
char readBuf[128];
int tmp;
char msg[128] = {0};
struct sockaddr_in c_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
if (argc != 3)
{
printf("param is not good\n");
exit(-1);
}
// 1. socket 配置 struct sockaddr_in 结构体,连接时再转换成 struct sockaddr * 结构体类型
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if (c_fd == -1)
{
perror("socket");
exit(-1);
}
// 2.connect
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &c_addr.sin_addr);
if (connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(-1);
}
// 连接成功
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if (n_read == -1)
{
perror("read");
}
else
{
printf("\nget:%s\n", readBuf);
}
// 不断的接收终端的命令发送至 c_fd
while (1)
{
memset(msg, 0, sizeof(msg));
printf("input: ");
fgets(msg,128,stdin);
write(c_fd, msg, strlen(msg));
}
return 0;
}
2.TCP并发服务器多进程
server.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
int iRet;
char readBuf[128];
pid_t pid;
int mark = 0;
char msg[128] = {0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if (argc != 3)
{
printf("param is not good\n");
exit(-1);
}
memset(&s_addr, 0, sizeof(struct sockaddr_in));
memset(&c_addr, 0, sizeof(struct sockaddr_in));
// 1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if (s_fd == -1)
{
perror("socket");
exit(-1);
}
// 2. bind 配置 struct sockaddr_in 结构体,绑定时再转换成 struct sockaddr * 结构体类型
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &s_addr.sin_addr);
iRet = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
if (iRet == -1)
{
perror("bind");
exit(-1);
}
// 3. listen
iRet = listen(s_fd, 10);
if (iRet == -1)
{
perror("listen");
exit(-1);
}
// 4. accept
int clen = sizeof(struct sockaddr_in);
while (1)
{
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if (c_fd == -1)
{
perror("accept");
break;
}
mark++;
printf("get connect: %s\n", inet_ntoa(c_addr.sin_addr));
pid = fork();
if(pid < 0){
perror("fork");
break;
}
// 接收到 connect 后用子进程处理
if (pid == 0)
{
sprintf(msg, "welcom No.%d client", mark); // 字符串拼接
write(c_fd, msg, strlen(msg));
// 在子进程里面不断的读取客户端发送到 c_fd 的内容
while (1)
{
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if (n_read == -1)
{
perror("read");
}
else if (n_read > 0)
{
printf("\nget command: %s \n", readBuf);
}
else
{
printf("client quit\n");
break;
}
}
}
}
return 0;
}
client.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int c_fd;
int n_read;
char readBuf[128];
int tmp;
char msg[128] = {0};
struct sockaddr_in c_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
if (argc != 3)
{
printf("param is not good\n");
exit(-1);
}
// 1. socket 配置 struct sockaddr_in 结构体,连接时再转换成 struct sockaddr * 结构体类型
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if (c_fd == -1)
{
perror("socket");
exit(-1);
}
// 2.connect
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &c_addr.sin_addr);
if (connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(-1);
}
// 连接成功
memset(readBuf, 0, sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);
if (n_read == -1)
{
perror("read");
}
else
{
printf("\nget:%s\n", readBuf);
}
// 不断的接收终端的命令发送至 c_fd
while (1)
{
memset(msg, 0, sizeof(msg));
printf("input: ");
gets(msg);
write(c_fd, msg, strlen(msg));
}
return 0;
}
四、UDP 编程API
bind:
- 绑定服务器:IP地址和端口号与套接字进行绑定
- 服务器是被动的,客户端是主动的,客户端不需要绑定ip和端口号(系统会自动分配),一般是先运行服务器再运行客户端
- 客户端的本地ip、本地port是我们调用sendto的时候linux系统底层自动给客户端分配的;分配端口的方式为随机分配,即每次运行系统给的port不一样
receivefrom:
- 阻塞等待客户端数据
- 接收UDP数据,也可以接收TCP数据 ,保存在src_addr指向的结构体中
- **相比于recv方法,可以拿到客户端的ip地址和端口号,便于发送数据给指定客户端 **
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(
int sockfd,
void *buf,
size_t len,
int flags,
struct sockaddr *src_addr,
socklen_t *addrlen
);
参数:
sockfd :数据报套接字,socket函数返回值
buf :内存地址,接收数据缓冲区
len :接收数据的大小
flags :标志位 0
src_addr :源地址addr结构体地址,用来保存数据的来源
addrlen :addr结构体的长度的地址
返回值:
成功则返回接收的字节数,
出错返回-1
sendto:
- 指定服务器(客户端)的IP地址和端口号,要发送的数据
#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
);
参数:
sockfd :数据报套接字,socket函数返回值
buf :内存地址,发送数据缓冲区
len :发送数据的大小
flags :标志位 0
src_addr :接收端的addr结构体地址
addrlen :addr结构体的长度的地址【这里不是取地址值,与receivfrom不一样】
返回值:
成功则返回发送的字节数,
出错返回-1
注意:可以发送0长度的UDP数据包
send 函数
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
参数:
- sockfd:socket函数返回的fd
- buffer:发送缓冲区首地址
- length:发送的字节
- flags:发送方式(通常为0,表示阻塞),作用和write一样
- MSG_DONTWAIT,非阻塞
- MSG_OOB:用于TCP类型的带外数据(out of band)
返回值:
- 成功:实际发送的字节数
- 失败:-1,并设置errno
注意:不能用TCP协议发送0长度的数据包,UDP可以
recv 函数
#include <sys/types.h>
#include <sys/socket.h>
int recv( SOCKET s, char FAR *buf, int len, int flags);
参数
- flag:一般填0,和read作用一样
- 特殊的标志:
- MSG_DONTWAIT
- MSG_OOB:读取带外数据
- MSG_PEEK:流
- 特殊的标志:
返回值:
- 成功:接收的字节数
- 失败:‐1
- 如果发送端关闭文件描述符或者关闭进程,则recv函数会返回0
五、UDP编程
UDP服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, const char *argv[])
{
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
int b_reuser = 1;
//地址映射,允许地址快速重新使用,因为四次
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuser,sizeof(int));
//2. 绑定服务器的IP地址和端口号
struct sockaddr_in serveraddr ={0};
memset(&serveraddr,0,sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8080);
//serveraddr.sin_addr.s_addr = inet_addr("0");
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示任意地址
int len = sizeof(serveraddr);
int ret = bind(sockfd, (struct sockaddr *)&serveraddr, len );
if(ret == -1)
{
perror("bind");
return -1;
}
//3.收发数据
struct sockaddr_in clientaddr={0};
socklen_t addrlen=sizeof(clientaddr);
char buf[64] = {0};
while(1)
{
memset(&clientaddr,0,sizeof(struct sockaddr_in));
//可以理解为将read函数写在了recvfrom里面,从buf里面读取
int n=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&clientaddr,&addrlen);
if(n<0)
{
perror("recvfrom");
continue;//继续读值
}
//将客户端的ip地址和端口号打印出来
//inet_ntoa只需要传递到sin_addr结构体即可,不用写到成员变量s_addr
printf("client ip:%s client port:%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
printf("message:%s\n", buf);
if(strcmp(buf,"quit")==0){
printf("client(%s,%d)is exiting\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
break;//退出
}
memset(buf, 0, 64); //数组清零
}
close(sockfd);
return 0;
}
UDP客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, const char *argv[])
{
if(argc<3)
{
printf("usage ./可执行文件 服务器IP地址 端口号");
return -1;
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
//服务器的IP地址和端口号
struct sockaddr_in serveraddr = {0},clientaddr={0};
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));//主机字节序转为网络字节序,atoi将字符串转化为整型数据
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);//点分十进制转化为整型数据
//serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
int len = sizeof(serveraddr);
//2.收发数据
char buf[64] = {0};
int l=sizeof(clientaddr);
printf("server ip:%s server port:%d\n", inet_ntoa(serveraddr.sin_addr), ntohs(serveraddr.sin_port));
while(1)
{
printf("input:");
fgets(buf,1024,stdin);//回车符号会当做字符保存,所以要吸收回车符
buf[strlen(buf)-1]='\0';//把buf字符串中的\n转化为\0
int n=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&serveraddr,len);
if(n<0)
{
perror("sendto");
return -1;
}
if(strcmp(buf,"quit")==0)break;
//printf("message:%s\n", buf);
memset(buf, 0, 64); //数组清零
}
close(sockfd);
return 0;
}
网络调试助手
- 模拟UDP的服务端,测试客户端代码是否正常
- 模拟UDP接收修改网络设置部分
- 模拟UDP发送修改目标主机部分
六、UDP实现双方聊天
UDP是并发服务器,所以客户端可以多个
服务端
//udp服务器的实现
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>
int main(int argc, char const* argv[])
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd; //文件描述符
struct sockaddr_in serveraddr; //服务器网络信息结构体
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制字符串ip地址转化为整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if (bind(sockfd, (struct sockaddr*)&serveraddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}
while (1)
{
//第四步:进行通信
char text[32] = "";
struct sockaddr_in clientaddr;
if (recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr*)&clientaddr, &addrlen) < 0)
{
perror("fail to recvfrom");
exit(1);
}
printf("[% s ‐ % d]: % s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), ext);
strcat(text, " *_*");
if (sendto(sockfd, text, sizeof(text), 0, (struct sockaddr*)&clientaddr, addrlen) < 0)
{
perror("fail to sendto");
exit(1);
}
}
//第四步:关闭文件描述符
close(sockfd);
return 0;
}
客户端
//udp客户端的实现
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>
int main(int argc, char const* argv[])
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd; //文件描述符
struct sockaddr_in serveraddr; //服务器网络信息结构体
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//客户端自己指定自己的ip地址和端口号,一般不需要,系统会自动分配
#if 0
struct sockaddr_in clientaddr;
clientaddr.sin_family = AF_INET;
clientaddr.sin_addr.s_addr = inet_addr(argv[3]); //客户端的ip地址
clientaddr.sin_port = htons(atoi(argv[4])); //客户端的端口号
if (bind(sockfd, (struct sockaddr*)&clientaddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}
#endif
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制字符串ip地址转化为整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:进行通信
char buf[32] = "";
while (1)
{
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) ‐ 1] = '\0';
if (sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("fail to sendto");
exit(1);
}
char text[32] = "";
if (recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr*)&serveraddr, &addrlen) < 0)
{
perror("fail to recvfrom");
exit(1);
}
printf("from server: %s\n", text);
}
//第四步:关闭文件描述符
close(sockfd);
return 0;
}
七、TCP实现双方聊天
在网络进程中,父进程等待客户端的服务请求,当这种请求到达时,父进程调用fork,使得子进程处理该请求,父进程继续等待下一个服务请求的到达
服务端
#include <stdio.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
int main (int argc,char ** argv){
int s_fd;
int c_fd;
int n_read;
int ret;
char readBuf[128];
char msg[128]={0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
if(argc !=3){
printf("param is not enough");
exit(-1);
}
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd ==-1){
perror("socket");
exit(-1);
}
s_addr.sin_family =AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
ret = bind(s_fd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr_in));
if (ret == -1)
{
perror("bind");
exit(-1);
}
ret = listen(s_fd,10);
{
perror("listen");
exit(-1);
}
int clen = sizeof(struct sockaddr_in);
while(1){
c_fd = accept(s_fd,(struct sockaddr*)&c_addr,&clen);
if(c_fd == -1 ){
perror("accept");
exit(-1);
}
//打印连接成功的客户端的IP地址
printf("get connect: %s\n", inet_ntoa(c_addr.sin_addr));
if(fork()==0){
if(fork()==0){
while(1){
memset(msg,0,sizeof(msg));
printf("input:");
fgets(msg,128,stdin);
write(c_fd,msg,strlen(msg));
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd,readBuf,128);
if(n_read == -1){
perror("read");
}else{
printf("get message from client:%s\n",readBuf);
}
}
break;
}
}
return 0;
}
客户端
#include <stdio.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
int main (int argc,char ** argv){
int c_fd;
int n_read;
char readBuf[128];
char msg[128]={0};
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
if(argc !=3){
printf("param is not enough");
exit(-1);
}
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd ==-1){
perror("socket");
exit(-1);
}
c_addr.sin_family =AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
if(connect(c_fd,(struct sockaddr*)&c_addr,sizeof(struct sockaddr))==-1){
perror("connect");
exit(-1);
}
while(1){
if(fork()==0){
while(1){
memset(msg,0,sizeof(msg));
printf("input:");
fgets(msg,128,stdin);
write(c_fd,msg,strlen(msg));
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd,readBuf,128);
if(n_read == -1){
perror("read");
}else{
printf("get message from server:%d,%s\n",n_read,readBuf);
}
}
break;
}
return 0;
}
执行效果:
注意:
- gcc并没有禁止gets. 只是会提示warning。如果设置了把所有warning处理为error才会导致无法使用。可以用fgets来代替。fgets(s, max_len, stdin);等效于gets(s)
- char s[200];
- fgets(s, 200, stdin);
九、TFTP协议介绍与通信过程
TFTP概述
- TFTP:简单文件传送协议
- 最初用于引导无盘系统,被设计用来传输小文件(因为基于无连接UDP设计的)
- FTP则是TCPx协议实现的应用层协议,可以支持复杂文件传输
特点:
- 基于UDP协议实现的应用层协议
- 不进行用户有效性认证
数据传输模式:
- octet:二进制模式
- netascii:文本模式
- mail:已经不再支持
TFTP通信过程
TFTP通信过程总结
- 1、服务器在69号端口等待客户端的请求(客户端请求包含两种:上传和下载)
- 2、服务器若批准此请求,则使用**
临时端口
**与客户端进行通信(实现并发) - 3、每个数据包的编号都有变化(从1开始)
- 4、每个数据包都要得到ACK的确认如果出现超时,则需要重新发送最后的包(数据或
ACK) - 5、数据的长度最大以512Byte传输
- 6、最后一次小于512Byte的数据意味着传输结束,断开连接
TFTP协议分析
了解协议是为了铜鼓协议来组包
- 操作码中上传是2,下载是1
- 模式分为文本模式和二进制模式
- 以上的0代表的是’\0’
- 不同的差错码对应不同的错误信息
TFTP客户端下载实现
要求:
- 使用TFTP协议,下载server上的文件到本地
实现思路
- 1、构造请求报文,送至服务器(69号端口)
- 2、等待服务器回应
- 3、分析服务器回应
- 4、接收数据,直到接收到的数据包小于规定数据长度
服务器端事先通过准备好的软件来使用,选择服务器文件目录地址和ip地址
模拟器地址下载链接
客户端代码流程图
客户端编写代码
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
void do_download(int sockfd, struct sockaddr_in serveraddr)
{
char filename[128] = "";
printf("请输入要下载的文件名: ");
scanf("%s", filename);
//给服务器发送消息,告知服务器执行下载操作
unsigned char text[1024] = "";
int text_len;
socklen_t addrlen = sizeof(struct sockaddr_in);
int fd;
int flags = 0;
int num = 0;
ssize_t bytes;
//构建给服务器发送的tftp指令并发送给服务器,例如:01test.txt0octet0
text_len = sprintf(text, "%c%c%s%c%s%c", 0, 1, filename, 0, "octet", 0);
if (sendto(sockfd, text, text_len, 0, (struct sockaddr*)&serveraddr, addrlen) < 0)
{
perror("fail to sendto");
exit(1);
}
while (1)
{
//接收服务器发送过来的数据并处理
if ((bytes = recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr*)&serveraddr, &addrlen)) < 0)
{
perror("fail to recvfrom");
exit(1);
}
//printf("操作码:%d, 块编号:%u\n", text[1], ntohs(*(unsigned short*)(text + 2)));
//printf("数据:%s\n", text+4);
//判断操作码执行相应的处理
if (text[1] == 5)
{
printf("error: %s\n", text + 4);//打印错误信息
return;
}
else if (text[1] == 3)
{
if (flags == 0)
{
//创建文件
if ((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 06
)) < 0)
{
perror("fail to open");
exit(1);
}
flags = 1;
}
//对比编号和接收的数据大小并将文件内容写入文件
if ((num + 1 == ntohs(*(unsigned short*)(text + 2))) && (bytes =516))
{
num = ntohs(*(unsigned short*)(text + 2));
if (write(fd, text + 4, bytes ‐ 4) < 0)
{
perror("fail to write");
exit(1);
}
//当文件写入完毕后,给服务器发送ACK
text[1] = 4;
if (sendto(sockfd, text, 4, 0, (struct sockaddr*)&serverddr, addrlen) < 0)
{
perror("fail to sendto");
exit(1);
}
}
//当最后一个数据接收完毕后,写入文件后退出函数
else if ((num + 1 == ntohs(*(unsigned short*)(text + 2))) && (byes < 516))
{
if (write(fd, text + 4, bytes ‐ 4) < 0)
{
perror("fail to write");
exit(1);
}
text[1] = 4;
if (sendto(sockfd, text, 4, 0, (struct sockaddr*)&serverddr, addrlen) < 0)
{
perror("fail to sendto");
exit(1);
}
printf("文件下载完毕\n");
return;
}
}
}
}
int main(int argc, char const* argv[])
{
if (argc < 2)
{
fprintf(stderr, "Usage: %s <server_ip>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
//创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //tftp服务器端的ip地,192.168.3.78
serveraddr.sin_port = htons(69); //tftp服务器的端口号默认是69
do_download(sockfd, serveraddr); //下载操作
return 0;
}
TFTP客户端下载上传实现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define ERRLOG(errmsg) do {\
perror(errmsg); \
exit(1); \
}while (0)
#define N 128
void do_help()
{
system("clear");
printf("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐\n");
printf("‐‐‐‐‐‐ 1. 下载 ‐‐‐‐‐‐\n");
printf("‐‐‐‐‐‐ 2. 上传 ‐‐‐‐‐‐\n");
printf("‐‐‐‐‐‐ 3. 退出 ‐‐‐‐‐‐\n");
printf("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐\n");
}
void do_download(int sockfd, struct sockaddr_in serveraddr)
{
char filename[N] = {};
printf("请输入要下载的文件名:");
scanf("%s", filename);
char data[1024] = "";
int data_len;
int fd;
int flags = 0;
int num = 0;
int recv_len;
//组数据并发送
data_len = sprintf(data, "%c%c%s%c%s%c", 0, 1, filename, 0, "octet", 0
if (sendto(sockfd, data, data_len, 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0)
{
ERRLOG("fail to sendto");
}
//接收数据并分析处理
socklen_t addrlen = sizeof(serveraddr);
while (1)
{
if ((recv_len = recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr*)&serveraddr, &addrlen)) < 0)
{
ERRLOG("fail to recvfrom");
}
//printf("%d ‐ %u\n", data[1], ntohs(*(unsigned short *)(data + 2)));
//printf("%s\n", data + 4);
if (data[1] == 5)
{
printf("error: %s\n", data + 4);
return;
}
else if (data[1] == 3)
{
//防止文件内容清空
if (flags == 0)
{
if ((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 06)) < 0)
{
ERRLOG("fail to open");
}
flags = 1;
}
//判断数据包的编号是否是上一次的编号加1
if (num + 1 == ntohs(*(unsigned short*)(data + 2)) && recv_len == 516)
{
//向文件写入数据
write(fd, data + 4, recv_len ‐ 4);
//组数据发送给服务器
data[1] = 4;
if (sendto(sockfd, data, 4, 0, (struct sockaddr*)&serveraddr, addrlen) < 0)
{
ERRLOG("fail to sendto");
}
num = ntohs(*(unsigned short*)(data + 2));
}
//接收到的最后一次的数据
else if (num + 1 == ntohs(*(unsigned short*)(data + 2)) && recv_len < 516)
{
write(fd, data + 4, recv_len ‐ 4);
break;
}
}
}
printf("文件下载成功\n");
}
void do_upload(int sockfd, struct sockaddr_in serveraddr)
{
char filename[N] = {};
printf("请输入要上传的文件名:");
scanf("%s", filename);
//打开文件并判断文件是否存在
int fd;
if ((fd = open(filename, O_RDONLY)) < 0)
{
if (errno == ENOENT)
{
printf("文件%s不存在,请重新输入\n", filename);
return;
}
else
{
ERRLOG("fail to open");
}
}
//组数据并发送给服务器执行上传功能
char data[1024] = {};
int data_len;
socklen_t addrlen = sizeof(serveraddr);
data_len = sprintf(data, "%c%c%s%c%s%c", 0, 2, filename, 0, "octet",
if (sendto(sockfd, data, data_len, 0, (struct sockaddr*)&serveraddr, addrlen) < 0)
{
ERRLOG("fail to sendto");
}
//接收服务器发送的数据并分析处理
int recv_len;
int num = 0;
ssize_t bytes;
while (1)
{
if ((recv_len = recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr*)&serveraddr, &addrlen)) < 0)
{
ERRLOG("fail to recvfrom");
}
//printf("%d ‐ %d\n", data[1], ntohs(*(unsigned short *)(data + 2)));
//printf("%s\n", data + 4);
if (data[1] == 4 && num == ntohs(*(unsigned short*)(data + 2)))
{
num++;
bytes = read(fd, data + 4, 512);
data[1] = 3;
*(unsigned short*)(data + 2) = htons(num);
if (bytes == 512)
{
if (sendto(sockfd, data, bytes + 4, 0, (struct sockaddr*)erveraddr, addrlen) < 0)
{
ERRLOG("fail to sendto");
}
}
else
{
if (sendto(sockfd, data, bytes + 4, 0, (struct sockaddr*)erveraddr, addrlen) < 0)
{
ERRLOG("fail to sendto");
}
break;
}
}
}
printf("文件上传完毕\n");
}
int main(int argc, char const* argv[])
{
int sockfd;
struct sockaddr_in serveraddr;
//创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
ERRLOG("fail to socket");
}
//填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(69);
system("clear");
printf("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐\n");
printf("‐‐‐‐请输入help查看帮助信息‐‐‐‐\n");
printf("‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐\n");
printf(">>> ");
char buf[N] = {};
NEXT:
fgets(buf, N, stdin);
buf[strlen(buf) ‐ 1] = '\0';
if (strncmp(buf, "help", 4) == 0)
{
do_help();
}
else
{
printf("您输入的有误,请重新输入\n");
goto NEXT;
}
int num;
while (1)
{
printf("input>>> ");
scanf("%d", &num);
switch (num)
{
case 1:
do_download(sockfd, serveraddr);
break;
case 2:
do_upload(sockfd, serveraddr);
break;
case 3:
close(sockfd);
exit(0);
break;
default:
printf("您输入的有误,请重新输入\n");
break;
}
}
return 0;
}
十、UDP广播
广播的概念
- 广播:由一台主机向该主机所在子网内的所有主机发送数据的方式,
例如192.168.3.103主机发送广播信息,则192.168.3.1~192.168.3.254所有主机都可以接收到数据 - 广播只能用UDP或原始IP实现,不能用TCP
广播的用途
单个服务器与多个客户主机通信时减少分组流通
以下几个协议都用到广播
- 1、地址解析协议(ARP)
- 2、动态主机配置协议(DHCP)
- 3、网络时间协议(NTP)
广播的特点
- 1、处于同一子网的所有主机都必须处理数据
- 2、UDP数据包会沿协议栈向上一直到UDP层
- 3、运行音视频等较高速率工作的应用,会带来大负【组播可以优化】
- 4、局限于局域网内使用
广播地址
{网络ID,主机ID}
定向广播地址:主机ID全1
- 1、例:对于192.168.220.0/24,其定向广播地址为192.168.220.255
- 2、通常路由器不转发该广播,也可以实现转发
受限广播地址:255.255.255.255
- 路由器从不转发该广播
广播与单播的对比
单播:【最基本的UDP服务器与客户端】
广播:
- ip地址的主机号变为255
- mac地址是广播对应的mac地址ff:ff:ff:ff:ff:ff
- 在链路层中,交换机会根据该mac地址发送给所以主机,主机识别到该mac地址不会丢弃而是打通通道到网络层
- 到达传输层会识别端口是否开启,才决定是否接收
套接字选项
#include <sys/socket.h>
int setsockopt(
int socket,
int level,
int option_name,
const void* option_value,
socklen_t option_len
);
功能:设置一个套接字的选项(属性)
参数:
- socket:文件描述符
- level:协议层次
- SOL_SOCKET 套接字层次
- IPPROTO_TCP tcp层次
- IPPROTO_IP IP层次
- option_name:选项的名称
SO_BROADCAST: 允许发送广播数据(SOL_SOCKET层次的)
- SO_RCVBUF:接收缓冲区大小 :
- SO_SNDBUF :发送缓冲区大小:
option_value:设置的选项的值
- int类型的值,存储的是bool的数据(1和0)
- 0 不允许
- 1 允许
- int类型的值,存储的是bool的数据(1和0)
- option_len:option_value的长度
返回值:
- 成功:0
- 失败:‐1
广播流程
发送者:
- 第一步:创建套接字 socket()
- 第二步:设置为允许发送广播权限 setsockopt()
- 第三步:向广播地址发送数据 sendto()
接收者:
- 第一步:创建套接字 socket()
- 第二步:将套接字与广播的信息结构体绑定 bind()
- 第三步:接收数据 recvfrom()
广播示例
发送者
//广播发送者代码实现
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>
int main(int argc, char const* argv[])
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd; //文件描述符
struct sockaddr_in broadcataddr; //服务器网络信息结构体
socklen_t addrlen = sizeof(broadcataddr);
//第一步:创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//第二步:设置为允许发送广播权限
int on = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) < 0)
{
perror("fail to setsockopt");
exit(1);
}
//第三步:填充广播信息结构体
broadcataddr.sin_family = AF_INET;
broadcataddr.sin_addr.s_addr = inet_addr(argv[1]); //192.168.3.255 255.255.255.255
broadcataddr.sin_port = htons(atoi(argv[2]));
//第四步:进行通信
char buf[128] = "";
while (1)
{
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) ‐ 1] = '\0';
if (sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&broad
cataddr, addrlen) < 0)
{
perror("fail to sendto");
exit(1);
}
}
return 0;
}
接收者
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>
int main(int argc, char const* argv[])
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd; //文件描述符
struct sockaddr_in broadcataddr;
socklen_t addrlen = sizeof(broadcataddr);
//第一步:创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//第二步:填充广播信息结构体
broadcataddr.sin_family = AF_INET;
broadcataddr.sin_addr.s_addr = inet_addr(argv[1]); //192.168.3.255 255.255.255.255
broadcataddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与广播信息结构体绑定
if (bind(sockfd, (struct sockaddr*)&broadcataddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}
//第四步:进行通信
char text[32] = "";
struct sockaddr_in sendaddr;
while (1)
{
if (recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr*)&sendaddr, &addrlen) < 0)
{
perror("fail to recvfrom");
exit(1);
}
printf("[%s ‐ %d]: %s\n", inet_ntoa(sendaddr.sin_addr), ntohs(sendaddr.sin_port), text);
}
return 0;
}
十一、UDP多播
多播的概念
多播:
- 数据的收发仅仅在同一分组中进行,所以多播又称之为组播
多播的特点:
- 1、多播地址标示一组接口
- 2、多播可以用于广域网使用
- 3、在IPv4中,多播是可选的
多播地址
- IPv4的D类地址是多播地址
- 十进制:224.0.0.1~239.255.255.254
- 十六进制:E0.00.00.01~EF.FF.FF.FE
- 多播地址向以太网MAC地址的映射
- 多播的mac地址是由当前以太网mac地址的前三个字节加上当前多播IP地址的后三个字节,但是倒数第24
多播过程
比起广播,多播具有可控性,只有加入多播组的接收者才可以接收数据,否则接收不到
- 数据链路层是不完备过滤,而网络层是完备过滤,如果不在多播组内则不能通过UDP传输层
多播流程
发送者:
- 第一步:创建套接字 socket()
- 第二步:向多播地址发送数据 sendto()
接收者:
- 第一步:创建套接字 socket()
第二步:设置为加入多播组 setsockopt()
- 第三步:将套接字与多播信息结构体绑定 bind()
- 第五步:接收数据
多播地址结构体
在IPv4因特网域(AF_INET)中,多播地址结构体用如下结构体ip_mreq表示
struct in_addr
{
in_addr_t s_addr;
};
struct ip_mreq
{
struct in_addr imr_multiaddr;//多播组IP
struct in_addr imr_interface;//将要添加到多播组的IP
};
多播套接口选项
#include <sys/socket.h>
int setsockopt(
int socket,
int level,
int option_name,
const void* option_value,
socklen_t option_len
);
功能:设置一个套接字的选项(属性)
参数:
-
socket:文件描述符
-
level:协议层次
- IPPROTO_IP IP层次
-
option_name:选项的名称
- IP_ADD_MEMBERSHIP 加入多播组
- IP_DROP_MEMBERSHIP 离开多播组
-
option_value:设置的选项的值
struct ip_mreq { struct in_addr imr_multiaddr; //组播ip地址 struct in_addr imr_interface; //主机地址 INADDR_ANY 任意主机地址(自动获取你的主机地址) };
-
option_len:option_value的长度
返回值:
- 成功:0
- 失败:‐1
加入多播组示例
发送者:
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>
int main(int argc, char const* argv[])
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd; //文件描述符
struct sockaddr_in groupcastaddr; //服务器网络信息结构体
socklen_t addrlen = sizeof(groupcastaddr);
//第一步:创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//第二步:填充组播信息结构体
groupcastaddr.sin_family = AF_INET;
groupcastaddr.sin_addr.s_addr = inet_addr(argv[1]); //224.x.x.x ‐ 239.x.x.x
groupcastaddr.sin_port = htons(atoi(argv[2]));
//第三步:进行通信
char buf[128] = "";
while (1)
{
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) ‐ 1] = '\0'; //"hello\n"‐‐>"hello\0"
if (sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&groupcastaddr, addrlen) < 0)
{
perror("fail to sendto");
exit(1);
}
}
return 0;
}
接收者:
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>
int main(int argc, char const* argv[])
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd; //文件描述符
struct sockaddr_in groupcastaddr;
socklen_t addrlen = sizeof(groupcastaddr);
//第一步:创建套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}
//第二步:设置为加入多播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(argv[1]);
mreq.imr_interface.s_addr = INADDR_ANY;
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
{
perror("fail to setsockopt");
exit(1);
}
//第三步:填充组播信息结构体
groupcastaddr.sin_family = AF_INET;
groupcastaddr.sin_addr.s_addr = inet_addr(argv[1]); //224.x.x.x ‐ 2
.x.x.x
groupcastaddr.sin_port = htons(atoi(argv[2]));
//第四步:将套接字与广播信息结构体绑定
if (bind(sockfd, (struct sockaddr*)&groupcastaddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}
//第五步:进行通信
char text[32] = "";
struct sockaddr_in sendaddr;
while (1)
{
if (recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr*)&sendaddr, &addrlen) < 0)
{
perror("fail to recvfrom");
exit(1);
}
printf("[%s ‐ %d]: %s\n", inet_ntoa(sendaddr.sin_addr), ntohs(sendaddr.sin_port), text);
}
return 0;
}
十二、HTTP协议
Webserver—HTTP协议(超文本协议)
概念
- 一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议
特点
- 1、支持C/S架构
- 2、简单快速:客户向服务器请求服务时,只需
传送请求方法和路径
,常用方法:GET、POST - 3、无连接:限制每次连接只处理一个请求
- 4、无状态:即如果后续处理需要前面的信息,它必须重传,这样可能导致每次连接传送的数据量会增大,采用多线程完成服务器的并发
Webserver通信过程
本质就是TCP通信,只是有特殊的格式要求,例如网址的都是:IP地址+端口号+网页名称
webserver编写
- 服务器收到的数据:
- 服务器应答的格式:
- 服务器接收到浏览器发送的数据之后,需要判断GET/后面跟的网页是否存在,如果存在则请求成功,发送指定的指令,并发送文件内容给浏览器,如果不存在,则发送请求失败的指令
案例:
首先要将网站放到服务器的指定路径下!!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define N 1024
#define ERR_LOG(errmsg) do {\
perror(errmsg); \
printf("%s ‐ %s ‐ %d\n", __FILE__, __func__, __LINE__); \
} while (0)
void* pthread_fun(void* arg)
{
int acceptfd = *(int*)arg;
char buf[N] = "";
//封装2个头
char head[] = "HTTP/1.1 200 OK\r\n" \
"Content‐Type: text/html\r\n" \
"\r\n";
char err[] = "HTTP/1.1 404 Not Found\r\n" \
"Content‐Type: text/html\r\n" \
"\r\n" \
"<HTML><BODY>File not found</BODY></HTML>";
//接收浏览器通过http协议发送的数据包
if (recv(acceptfd, buf, N, 0) < 0)
{
ERR_LOG("fail to recv");
}
printf("*****************************\n\n");
printf("%s\n", buf);
// int i;
// for(i = 0; i < 200; i++)
// {
// printf("[%c] ‐ %d\n", buf[i], buf[i]);
// }
printf("\n*****************************\n");
//通过获取的数据包中得到浏览器要访问的网页文件名
//GET /about.html http/1.1
char filename[128] = "";
sscanf(buf, "GET /%s", filename); //sscanf函数与空格结束,所以直接可以获取文件名
if (strncmp(filename, "HTTP/1.1", strlen("http/1.1")) == 0)
{
strcpy(filename, "about.html");
}
printf("filename = %s\n", filename);
char path[128] = "./sqlite/";
strcat(path, filename);
//通过解析出来的网页文件名,查找本地中有没有这个文件
int fd;
if ((fd = open(path, O_RDONLY)) < 0)
{
//如果文件不存在,则发送不存在对应的指令
if (errno == ENOENT)
{
if (send(acceptfd, err, strlen(err), 0) < 0)
{
ERR_LOG("fail to send");
}
close(acceptfd);
pthread_exit(NULL);
}
else
{
ERR_LOG("fail to open");
}
}
//如果文件存在,先发送指令告知浏览器
if (send(acceptfd, head, strlen(head), 0) < 0)
{
ERR_LOG("fail to send");
}
//读取网页文件中的内容并发送给浏览器
ssize_t bytes;
char text[1024] = "";
while ((bytes = read(fd, text, 1024)) > 0)
{
if (send(acceptfd, text, bytes, 0) < 0)
{
ERR_LOG("fail to send");
}
}
pthread_exit(NULL);
}
int main(int argc, char const* argv[])
{
if (argc < 3)
{
fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0])
exit(1);
}
int sockfd, acceptfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
ERR_LOG("fail to socket");
}
//将套接字设置为允许重复使用本机地址或者为设置为端口复用
int on = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
ERR_LOG("fail to setsockopt");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if (bind(sockfd, (struct sockaddr*)&serveraddr, addrlen) < 0)
{
ERR_LOG("fail to bind");
}
//第四步:将套接字设置为被动监听状态
if (listen(sockfd, 5) < 0)
{
ERR_LOG("fail to listen");
}
while (1)
{
//第五步:阻塞等待客户端的连接请求
if ((acceptfd = accept(sockfd, (struct sockaddr*)&clientaddr, &addrlen)) < 0)
{
ERR_LOG("fail to accept");
}
//打印客户端的信息
printf("%s ‐‐ %d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clien
taddr.sin_port));
//创建线程接收数据并处理数据
pthread_t thread;
if (pthread_create(&thread, NULL, pthread_fun, &acceptfd) != 0)
{
ERR_LOG("fail to pthread_create");
}
pthread_detach(thread);
}
return 0;
}