相同点:都是传输层的协议
不同点:
TCP协议:是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。
适用情况:适合于对传输质量要求较高,以及传输大量数据的通信。在需要可靠数据传输的场合,通常使用TCP协议。
如:MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议。
UDP协议:用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:发送小尺寸数据(如对DNS服务器进行IP地址查询时)在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)适合于广播/组播式通信中,通常使用TCP协议。
如:MSN/QQ等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议;流媒体、VOD、VoIP等网络多媒体服务中通常采用UDP方式进行实时数据传输。
服务器端:
1.创建套接字 socket()
2.填充服务器的网络信息结构体
3.绑定服务器的套接字和网络信息结构体 bind()
4.将服务器的套接字设置成被动监听状态 listen()
5.阻塞等待客户端的连接 accept()
6.收发数据--read/write
7.关闭套接字 close()
客户端:
1.创建套接字 socket()
2.填充服务器的网络信息结构体
3.与服务器建立连接 connect()
4.收发数据--read/write
5.关闭套接字 close()
示意图:
3.函数说明int socket(int domain, int type, int protocol);
功能:创建通信的端点,返回文件描述符(当前进程未打开的编号最小的)
#include
#include
参数:
@domain:指定通信域
AF_UNIX, AF_LOCAL:本地通信使用
AF_INET:IPV4使用
AF_INET6:IPV6使用
AF_PACKET:原始套接字使用
@type:指定套接字类型
SOCK_STREAM:TCP使用
SOCK_DGRAM:UDP使用
SOCK_RAW:原始套接字使用
@protocol:附加协议
如果使用的是TCP或者UDP 此处传0 即可
返回值:成功返回文件描述符,失败返回-1,置位错误码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将套接字和网络信息结构体进行绑定
#include
#include
参数:
@sockfd: socket函数返回的文件描述符
@addr: 要绑定的网络信息结构体
@addrlen: 网络信息结构体的长度
返回值:成功返回0,失败返回-1,置位错误码
int listen(int sockfd, int backlog);
功能:将套接字设置成被动监听状态
#include
#include
参数:
@sockfd: socket函数返回的文件描述符
@backlog: 最大监听队列的长度,表示同时支持的能连接的客户端的个数
一般不关心,设置成 5 或者 10 都可以,不能是 0
返回值:成功返回0,失败返回-1,置位错误码
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
功能:阻塞等待客户端的连接,有客户端连接后, 会返回一个新的文件描述符,专门用于和该客户端通信
#include
参数:
@sockfd: socket函数返回的文件描述符
@address: 如果想保存客户端的信息,就传一个地址 sockaddr_in
如果不关心,可以传 NULL
@backlog: address长度
如果不关心,可以传 NULL
返回值:成功返回用于和客户端通信的文件描述符,失败返回-1,置位错误码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:与服务器建立连接
#include
#include
参数:
@sockfd: socket函数返回的文件描述符
@address: 服务器的网络信息结构体,用于标记连接到哪个服务器程序
@address_len:address长度
返回值:成功返回0,失败返回-1,置位错误码
示例:
//服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(){
//1.创建套接字 ---相当于 买了一个手机
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
perror("socket error");
exit(-1);
}
//创建服务器网络信息结构体 ---相当于办了一张卡
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
//网络字节序的端口号,可以是 8888 9999 6789 等都可以
server_addr.sin_port = htons(8888);
//IP地址
//不能随便填,可以填自己主机的IP地址
//如果只是在本地测试,也可以填 127.0.0.1
server_addr.sin_addr.s_addr = inet_addr("192.168.70.95");
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定---相当于把卡插入手机里
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
perror("bind error");
exit(-1);
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
perror("listen error");
exit(-1);
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
//5.阻塞等待客户端连接
int acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
perror("accept error");
exit(-1);
}
printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
//6.与客户端通信
char buff[128] = {0};
read(acceptfd, buff, 128);
printf("%s-%d:[%s]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
strcat(buff, "--server");
write(acceptfd, buff, 128);
//7.关闭套接字
close(acceptfd);
close(sockfd);
return 0;
}
//客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(){
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
perror("socket error");
exit(-1);
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体 --需要指定连接哪个服务器
server_addr.sin_family = AF_INET;
//网络字节序的端口号,可以是 8888 9999 6789 等都可以
server_addr.sin_port = htons(8888);
//IP地址
//不能随便填,可以填自己主机的IP地址
//如果只是在本地测试,也可以填 127.0.0.1
server_addr.sin_addr.s_addr = inet_addr("192.168.70.95");
socklen_t addrlen = sizeof(server_addr);
//3.与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
perror("connect error");
exit(-1);
}
//4.与服务器通信
char buff[128] = {0};
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
write(sockfd, buff, 128);
read(sockfd, buff, 128);
printf("收到回复:[%s]\n", buff);
//5.关闭套接字
close(sockfd);
return 0;
}
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:在套接字中接收数据
#include
#include
参数:
@sockfd: 在哪个套接字中接
@buf: 存放接收的数据的缓冲区
@len: 想要接多少个字节
@flags: 如果设置成 MSG_DONTWAIT 表示非阻塞
如果是0 和read 的用法就一样了
返回值:成功返回实际接收的字节数,失败返回-1,置位错误码
如果发送方关闭,recv会返回0
注:下面三种用法是等价的
read(sockfd, buff, 128);
recv(sockfd, buff, 128, 0);
recvfrom(sockfd, buf, 128, 0, NULL, NULL);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:在套接字中接收数据
#include
#include
参数:
@sockfd: 向哪个套接字发
@buf: 要发送的数据的首地址
@len: 想要发送多少个字节
@flags: 如果设置成 MSG_DONTWAIT 表示非阻塞
如果是0 和write 的用法就一样了
返回值:成功返回实际发送的字节数,失败返回-1,置位错误码
如果接收方关闭,send会返回0
如果第二次再send,会产生 SIGPIPE 信号
注:下面三种用法是等价的
write(sockfd, buff, 128);
send(sockfd, buff, 128, 0);
sendto(sockfd, buf, 128, 0, NULL, NULL);
示例:实现简单的文件下载功能
要求:客户端和服务器 要在不同的路径下运行服务器运行后,客户端连接到服务器,给服务器发送要下载的文件名
服务器判断文件是否存在(当前路径下),不管存在不存在都要通知一下客户端
如果存在,则打开文件,读取文件内容发给客户端,客户端创建并打开一个新文件,接收文件内容,并写入该文件。
//服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
typedef struct __MSG{
char buff[N];
int bytes;
}msg_t;
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&server_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[N] = {0};
int acceptfd = 0;
ACCEPT:
//5.阻塞等待客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
ERRLOG("accept error");
}
printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
RENAME:
//接收客户端要下载的文件名
if(-1 == recv(acceptfd, buff, N, 0)){
ERRLOG("recv error");
}
printf("客户端要下载的文件名为:[%s]\n", buff);
//判断文件是否存在
int fd = open(buff, O_RDONLY);
if(-1 == fd){
//文件不存在
if(errno == ENOENT){
printf("文件[%s]不存在\n",buff);
//告诉客户端文件不存在
if(-1 == send(acceptfd, "****NO EXIST****", N, 0)){
perror("send error");
goto RENAME;
}
}else{
ERRLOG("open error");
}
}
printf("文件[%s]存在\n",buff);
//文件存在 也要通知客户端
if(-1 == send(acceptfd, "****EXIST****", N, 0)){
ERRLOG("send error");
}
int bytes = 0;
#if 0
//循环读取文件内容并发送给客户端
while((bytes = read(fd, buff, N))>0){
if(-1 == send(acceptfd, buff, bytes, 0)){
ERRLOG("send error");
}
memset(buff, 0, N);
}
//TCP的粘包问题
//TCP的底层有一个 Nagel算法,将一定短的时间内的小的数据包
//组装给一个整体,一次性的发给接收端,接收端是无法判断数据类型的
//所以发送的数据就会有冲突,就可能产生问题。
//解决方式1:将文件内容和 结尾的标志 产生点时间差发送
sleep(1);
//发送传输完毕的信息
if(-1 == send(acceptfd, "****OVER****", 13, 0)){
ERRLOG("send error");
}
#endif
//解决方式2:只要保证每次收发的数据包一样大就不会出现这个问题
//可以定义一个结构体,每次收发数据都是结构体的大小
msg_t msg;
memset(&msg, 0, sizeof(msg));
//循环读取文件内容并发送给客户端
while((bytes = read(fd, msg.buff, N))>0){
msg.bytes = bytes;
if(-1 == send(acceptfd, &msg, sizeof(msg), 0)){
ERRLOG("send error");
}
memset(&msg, 0, sizeof(msg));
}
//发送传输完毕的信息
strcpy(msg.buff, "****OVER****");
msg.bytes = 0;
if(-1 == send(acceptfd, &msg, sizeof(msg), 0)){
ERRLOG("send error");
}
close(acceptfd);
goto ACCEPT;
close(sockfd);
return 0;
}
//客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
typedef struct __MSG{
char buff[N];
int bytes;
}msg_t;
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("connect error");
}
char filename[N] = {0};
char buff[N] = {0};
int fd = 0;
RENAME:
printf("请输入要下载的文件:");
scanf("%s", filename);
//将要下载的文件发送给服务器
if(-1 == send(sockfd, filename, N, 0)){
ERRLOG("send error");
}
//接收文件是否存在的信息
if(-1 == recv(sockfd, buff, N, 0)){
ERRLOG("recv error");
}
if(0 == strcmp(buff, "****NO EXIST****")){
printf("文件不存在\n");
goto RENAME;
}else if(0 == strcmp(buff, "****EXIST****")){
//创建并打开并清空一个文件,准备下载文件内容
if(-1 == (fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0664))){
ERRLOG("open error");
}
}
int bytes = 0;
#if 0
//循环接收文件内容
while((bytes = recv(sockfd, buff, N, 0))>0){
if(0 == strcmp(buff, "****OVER****")){
break;
}
if(-1 == write(fd, buff, bytes)){
ERRLOG("write error");
}
memset(buff, 0, N);
}
#endif
msg_t msg;
memset(&msg, 0, sizeof(msg));
while(recv(sockfd, &msg, sizeof(msg), 0)>0){
if(msg.bytes == 0){
break;
}
if(-1 == write(fd, msg.buff, msg.bytes)){
ERRLOG("write error");
}
memset(&msg, 0, sizeof(msg));
}
close(fd);
printf("文件下载完成\n");
//关闭套接字
close(sockfd);
return 0;
}
二、UDP网络编程
1.UDP网络编程流程
服务器流程:
1.创建用户数据报式套接字
2.填充服务器网络信息结构体
3.绑定套接字与网络信息结构体
4.收发数据 recvfrom/ sendto
5.关闭套接字
客户端流程:
1.创建用户数据报式套接字
2.填充服务器网络信息结构体
3.收发数据 recvfrom/ sendto
4.关闭套接字
示意图:
2.函数说明ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
功能:在套接字中接收数据
#include
#include
参数:
前四个参数和 recv 的用法一样
后两个参数和 accept 的后两个参数用法一样,是用来保存客户端的信息的
返回值:成功返回实际接收到的数据的大小,失败返回-1,置位错误码
如果发送方关闭,recvfrom会返回0
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
功能:向套接字发送数据
#include
#include
参数:
前四个参数和 send 的用法一样
后两个参数和 connect 的后两个参数用法一样,是指定发送给谁的
返回值:成功返回实际发送的字节数,失败返回-1,置位错误码
如果接收方关闭,sendto会返回0
如果第二次再sendto,会产生 SIGPIPE 信号
示例:
//服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建用户数据报式套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&server_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[N] = {0};
while(1){
//接收数据,如果想要给对方回应,就必须保存对方的网络信息结构体
//如果不回应,后两个参数写 NULL 也行
if(-1 == recvfrom(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, &clientaddrlen)){
ERRLOG("recvfrom error");
}
printf("%s(%d):%s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
//组装应答信息
strcat(buff, "--hqyj");
if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
ERRLOG("sendto error");
}
memset(buff, 0, N);
}
//关闭套接字
close(sockfd);
return 0;
}
//客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建用户数据报套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
char buff[N] = {0};
while(1){
printf("input your msg:");
fgets(buff, N, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(0 == strcmp(buff, "quit")){
break;
}
if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("sendto error");
}
//因为客户端已经知道服务器的网络信息了 所以后两个参数可以传NULL
if(-1 == recvfrom(sockfd, buff, N, 0, NULL, NULL)){
ERRLOG("recvfrom error");
}
printf("recv:[%s]\n", buff);
memset(buff, 0, N);
}
//关闭套接字
close(sockfd);
return 0;
}
3.TCP和UDP使用总结及注意事项
TCP网络编程流程:
1.客户端是不需要绑定自己的网络信息结构体的,因为运行的时候, *** 作系统会自动分配,如果想要指定IP地址和端口号,也可以调用bind函数。
2.服务器端的accept函数的后两个参数,是用来保存连接过来的客户端的网络信息结构体的。对于TCP而言,如果不保存客户端的网络信息结构体,也是可以和对方通信的,因为accept函数会返回一个新的文件描述符,专门用于和这个客户端通信,服务器的套接字和客户端是一对一的关系。
3.TCP可以使用read/write 、recv/send 、 recvfrom/sendto 收发数据。
4.服务器的accept本质是一个阻塞的读,客户端的connect是一个写。相当于客户端将自己的网络信息结构体发给了服务器。
5.TCP服务器是一个循环服务器,同一时间只能处理同一个客户端的请求。原因是有两个阻塞函数会相互影响 accept 和 recv。
UDP网络编程流程:
1.UDP是无连接的,所以UPD的客户端无需绑定自己的网络信息结构体,如果想绑定,也可以使用bind函数绑定。UDP是使用 recvfrom和sendto进行数据收发的,sendto的后两个参数,表示数据要发给谁,recvfrom的后两个参数是用于接收,发送端的网络信息结构体的,如果想要给对方回消息,就必须要保存,如果特殊场景下服务器只负责收数据,recvfrom的后两个参数置NULL 也行。
sendto相当于 connect 和 send 的二合一,recvfrom相当于 accept 和 recv 的二合一。
2.UDP客户端也可以使用connect函数向去将自己的网络信息结构体发给服务器,但是UDP使用connect也不会产生三次握手的过程。使用connect之后,UDP的客户端也可以使用send和recv函数和服务器通信了。UDP服务器端不可以这么做。
3.UDP服务器本身就是一个并发服务器。原因是UDP服务器只有一个阻塞的函数 recvfrom
注:并发服务器在下一篇博客(C语言中IO模型实现并发服务器)中详细讲。
三、TFTP协议简介TFTP:简单文件传送协议,最初用于引导无盘系统,被设计用来传输小文件
特点:
基于UDP协议实现
不进行用户有效性认证
数据传输模式:
octet:二进制模式
netascii:文本模式
mail:已经不再支持
1.TFTP通信过程1、服务器在69号端口等待客户端的请求
2、服务器若批准此请求,则使用临时端口与客户端进行通信
3、每个数据包的编号都有变化(从1开始)
4、每个数据包都要得到ACK的确认如果出现超时,则需要重新发送最后的包(数据或ACK)
5、数据的长度以512Byte传输
6、小于512Byte的数据意味着传输结束
2.TFTP 协议分析 3.错误码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.
4.代码实现编写TFTP客户端程序 实现 从TFTP 服务器下载文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
char filename[32] = {0};
char buff[600] = {0};
int bytes = 0;
unsigned short num = 0;// *** 作码
unsigned short block_code = 0;//块编号或者差错码
unsigned short block_count = 0;//已经收到的块的编号
char text[600] = {0};//文件内容或者差错信息
int fd = 0;
//1.创建用户数据报套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
LOOP: //LOOP要放到 赋值端口号前面 因为69已经被覆盖了
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
printf("input filename:");
scanf("%s", filename);
//赋值 *** 作码的方式1
//*(unsigned short *)buff = htons(1);
//赋值 *** 作码的方式2:一个字节一个字节赋值
//buff[0] = 0;
//buff[1] = 1;
//方式3 使用 sprintf 封包
bytes = sprintf(buff,"%c%c%s%c%s%c",0,1,filename,0,"octet",0);
//发送请求
if(-1 == sendto(sockfd, buff, bytes, 0, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("sendto error");
}
while(1){
//此处需要重新保存服务器的网络信息结构体
//因为服务器使用临时端口 而不是 69端口给我们发数据的
if(-1 == (bytes = recvfrom(sockfd, buff, 600, 0, (struct sockaddr *)&server_addr, &addrlen))){
ERRLOG("recvfrom error");
}
//解析 *** 作码
num = ntohs(*(unsigned short *)buff);
//解析块编号或者差错码
block_code = ntohs(*(unsigned short *)(buff+2));
//文件内容或差错信息
strncpy(text, buff+4, bytes-4);
//printf("num = %d code = %d\n", num, block_code);
//printf("%s\n", buff+4);
if(5 == num){
printf("num = %d , text = [%s]\n", num, text);
goto LOOP;
}else if(3 == num && block_code == block_count+1){//对包的编号做校验
block_count = block_code;
//第一个数据包过来是创建文件
if(1 == block_code){
if(-1 == (fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0664))){
ERRLOG("open error");
}
}
//将内容写入文件
if(-1 == write(fd, text, bytes-4)){
ERRLOG("write error");
}
//组装ACK
memset(buff, 0, sizeof(buff));
*(unsigned short *)buff = htons(4);
*(unsigned short *)(buff+2) = htons(block_code);
//发送ACK
if(-1 == sendto(sockfd, buff, 4, 0, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("sendto error");
}
}
//文件传输完毕
if(bytes-4 < 512){
break;
}
}
//关闭套接字
close(sockfd);
return 0;
}
四、原始套接字(拓展)
1.概念
1.一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心
2.可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用
3.开发人员可发送自己组装的数据包到网络上
4.广泛应用于高级网络编程
5.网络专家、黑客通常会用此来编写奇特的网络程序
2.创建原始套接字int socket(int domain, int type, int protocol);
功能:创建套接字
#include
#include
#include
#include
参数:
@domain:通信域,协议族
AF_UNIX, AF_LOCAL:本地通信使用
AF_INET:IPV4使用
AF_INET6:IPV6使用
AF_PACKET:原始套接字使用
@type:套接字类型
SOCK_STREAM:TCP使用
SOCK_DGRAM:UDP使用
SOCK_RAW:原始套接字使用
@protocol:附加协议
ETH_P_IP IPV4数据包
ETH_P_ARP ARP数据包
ETH_P_ALL 任何协议类型的数据包
返回值:成功返回文件描述符,失败返回-1,置位错误码
示例:
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
int sockfd;
//创建一个原始套接字
//注意 三个参数 都要设置
if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) == -1)
{
ERRLOG("socket error");
}
printf("sockfd = %d\n", sockfd);
return 0;
}
3.从网卡上直接读取数据包并解析
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s (%s):%d\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
int socket_fd = -1;
if(-1 == (socket_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)))){
ERRLOG("socket error");
}
//直接从网卡上读取数据并解析
unsigned char buff[1600] = {0};
unsigned char mac_dest[18] = {0};
unsigned char mac_src[18] = {0};
unsigned short mac_type = 0;
unsigned short ip_len = 0;
unsigned char ip_ttl = 0;
unsigned char ip_type = 0;
unsigned char ip_src[16] = {0};
unsigned char ip_dest[16] = {0};
unsigned short port_src = 0;
unsigned short port_dest = 0;
while(1){
printf("---------------recving----------------\n");
if(-1 == recvfrom(socket_fd, buff, 1600, 0, NULL, NULL)){
ERRLOG("recvfrom error");
}
//----------------解析以太网头------------------
//目的 mac
sprintf(mac_dest, "%x:%x:%x:%x:%x:%x", buff[0], buff[1], buff[2], buff[3], buff[4], buff[5]);
//源mac
sprintf(mac_src, "%x:%x:%x:%x:%x:%x", buff[6], buff[7], buff[8], buff[9], buff[10], buff[11]);
//协议类型
mac_type = ntohs(*(unsigned short *)(buff+12));
printf("以太网头----mac_dest:[%s], mac_src:[%s], mac_type:[%#x]\n", mac_dest, mac_src, mac_type);
if(mac_type == 0x0800){
//----------------解析IP头------------------
ip_len = ntohs(*(unsigned short *)(buff+16));
ip_ttl = *(buff+22);
ip_type = *(buff+23);
sprintf(ip_src, "%d.%d.%d.%d",buff[26], buff[27], buff[28], buff[29]);
sprintf(ip_dest, "%d.%d.%d.%d",buff[30], buff[31], buff[32], buff[33]);
printf("IP头----ip_len:[%d], ip_ttl:[%d], ip_type:[%d], ip_src:[%s], ip_dest[%s]\n",
ip_len, ip_ttl, ip_type, ip_src, ip_dest);
if(ip_type == 6){//TCP
//----------------解析TCP头------------------
//源端口号
port_src = ntohs(*(unsigned short *)(buff+34));
//目的端口号
port_dest = ntohs(*(unsigned short *)(buff+36));
printf("TCP头----port_src:[%d], port_dest:[%d]\n", port_src, port_dest);
}else if(ip_type == 17){//UDP
//----------------解析UDP头------------------
//源端口号
port_src = ntohs(*(unsigned short *)(buff+34));
//目的端口号
port_dest = ntohs(*(unsigned short *)(buff+36));
printf("UDP头----port_src:[%d], port_dest:[%d]\n", port_src, port_dest);
}else if(ip_type == 1){
printf("ICMP头----\n");
}else if(ip_type == 2){
printf("IGMP头----\n");
}
}else if(mac_type == 0x0806){
printf("ARP头----\n");
}else if(mac_type == 0x8035){
printf("RARP头----\n");
}
printf("\n");
printf("-----------------------------------------------------------------\n");
printf("\n");
}
close(socket_fd);
return 0;
}
4.arp协议的使用
arp协议:地址解析协议,通过对方的ip地址,获取对方Mac地址
运行机制:源主机进行组包,指定自己的ip地址和mac地址,指定对方的ip地址,通过广播的信息获取,所以mac地址是广播的mac地址(全是f),设置arp请求将数据包发送给对方,交换机接收到这个数据包之后,一看是目的mac地址是广播的,所以会将这个数据包发送给当前网段下所有的主机,主机接收到这个数据包之后对比目的ip地址,如果不是自己的则丢弃,如果是自己的,将组包之后以arp应答的方式将数据包发送给获取者。
arp 数据报:
接下来,我要让ubuntu获取windows的mac地址:
以太网头:
Dest Mac:目的mac地址,广播的mac地址,全是f
Src Mac:源mac地址,ubuntu的mac地址,00:0c:29:a1:f9:68
帧类型:后面跟的协议类型,如果是arp,则指定为0x0806
arp头:
硬件类型:以太网,1
协议类型:IP地址,0x0800
硬件地址长度:6
协议地址长度:4
OP:请求还是应答,1
1(ARP请求),2(ARP应答),3(RARP请求),4(RARP应答)
发送端以太网地址:源mac地址,ubuntu的mac地址,00:0c:29:a1:f9:68
发送端ip地址:源ip地址:ubuntu的ip地址,192.168.70.95
目的以太网地址:目的mac地址,未知,可以传0
目的ip地址:目的ip地址,windows的ip地址,192.168.70.8
代码实现:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s (%s):%d\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
int socket_fd = -1;
if(-1 == (socket_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)))){
ERRLOG("socket error");
}
//使用arp协议通过对方ip地址获取对方mac地址
//组包
unsigned char buff[42] = {
//以太网头
//目标mac
0xff,0xff,0xff,0xff,0xff,0xff,
//源mac
0x00,0x0c,0x29,0xa1,0xf9,0x68,
//协议类型
0x08,0x06,
//arp头
//硬件类型 1(以太网)
0, 1,
//协议类型 0800(ip地址)
0x08, 0x00,
//硬件地址长度
6,
//协议地址长度
4,
//op 1 arp请求 2 arp应答
0, 1,
//源mac--ubuntu的mac
0x00,0x0c,0x29,0xa1,0xf9,0x68,
//源IP地址
192,168,60,123,
//目的mac ----不确定 全0即可
0, 0, 0, 0, 0, 0,
//目的IP地址
192,168,60,112
};
//将arp请求报文发送出去,通过 ens37 发送出去
//使用ioctl函数获取本机网络接口
struct ifreq ethreq;
strncpy(ethreq.ifr_name, "ens37", IFNAMSIZ);//IFNAMSIZ 16
if(ioctl(socket_fd, SIOCGIFINDEX, ðreq) == -1)
{
perror("fail to ioctl");
exit(1);
}
//设置本机网络接口
struct sockaddr_ll sll;
bzero(&sll, sizeof(sll));
sll.sll_ifindex = ethreq.ifr_ifindex;
//将数据包发送给网卡
if(sendto(socket_fd, buff, 42, 0, (struct sockaddr *)&sll, sizeof(sll)) == -1)
{
ERRLOG("sendto error");
}
//接收对方的数据包并解析出mac地址
unsigned char mac[18] = {0};
memset(buff, 0, 42);
while(1)//要循环获取,直接一次可能会接不到
{
if(recvfrom(socket_fd, buff, 42, 0, NULL, NULL) == -1)
{
ERRLOG("recvfrom error");
}
//先判断数据包是否是arp数据包
if(ntohs(*(unsigned short *)(buff+12)) == 0x0806)
{
//再判断是否是arp应答
if(ntohs(*(unsigned short *)(buff+20)) == 2)
{
sprintf(mac, "%x:%x:%x:%x:%x:%x", buff[22], buff[23], buff[24], buff[25], buff[26], buff[27]);
printf("%d.%d.%d.%d -- %s\n", buff[28], buff[29], buff[30], buff[31], mac);
break;
}
}
}
close(socket_fd);
return 0;
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)