目录
socket
套接字概念
最早的套接字和共享内存,消息队列,管道一样,只能实现一个主机内部的进程间通信。
后期加入了TCP/IP协议,使得套接字能够支持不同主机之间的进程间通信。
socket函数,可以在内核空间中创建两块缓冲区,供于发送数据,接收数据。
socket函数
功能:在内核空间中创建接收缓冲区和发送缓冲区,并在用户空间获取到该缓冲区的文件描述符;
用户可以通过该文件描述符操作缓冲区;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
int 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)
int type:
SOCK_STREAM: 字节流式套接字,流式套接字,----》默认指定TCP协议
SOCK_DGRAM: 数据报式套接字,报式套接字,----> 默认指定UDP协议。
SOCK_RAW: 原始套接字,协议需要在第三个参数指定。
int protocol:0,使用默认协议。
IPPROTO_TCP IPPROTO_UDP
返回值:
成功,返回的套接字文件描述符 >0;
失败,返回-1,更新errno;
TCP编程
TCP流程图
TCP编程
socket
功能:在内核空间中创建接收缓冲区和发送缓冲区,并在用户空间获取到该缓冲区的文件描述符;
用户可以通过该文件描述符操作缓冲区;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
int 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)
int type:
SOCK_STREAM: 字节流式套接字,流式套接字,----》默认指定TCP协议
SOCK_DGRAM: 数据报式套接字,报式套接字,----> 默认指定UDP协议。
SOCK_RAW: 原始套接字,协议需要在第三个参数指定。
int protocol:0,使用默认协议。
IPPROTO_TCP IPPROTO_UDP
返回值:
成功,返回的套接字文件描述符 >0;
失败,返回-1,更新errno;
bind
功能:绑定地址信息到套接字文件中;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
int sockfd:指定要绑定到哪个套接字文件上,填对应的文件描述符;
struct sockaddr *addr:通用地址信息结构体,真实的地址信息结构体根据地址族指定。
绑定IP和端口号到服务器套接字上;
AF_INET: man 7 ip
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */ 必须填AF_INET
in_port_t sin_port; /* port in network byte order */ 端口号的网络字节序:1024~49151
struct in_addr sin_addr; /* internet address */ 本机IP地址的网络字节序。ifconfig
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
socklen_t addrlen:真实的地址信息结构体的大小;
返回值:
成功,返回0;
失败,返回-1,更新errno;
- bind: Address already in use : 端口号被占用
- bind: Cannot assign requested address IP地址错误
listen
功能:将套接字设置为被动监听状态,监听是否有客户端连接.
让内核去维护两个队列:未完成连接的队列,已完成连接的队列;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数:
int sockfd:指定要将哪个文件描述符,设置为被动监听状态;
int backlog:指定未完成连接队列的容量; 允许同时有多少个客户端未完成连接,不要填0,1即可,一般填128;
返回值:
成功,返回0;
失败,返回-1,更新errno;
accept(阻塞函数)
功能:从已完成连接的队列头中获取一个客户端信息,生成一个新的文件描述符,该文件描述符才是通信使用的文件描述符;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
int sockfd:被转换为监听状态的文件描述符;
struct sockaddr *addr:通用地址信息结构体,真实的地址信息结构体根据地址族指定。
存储获取到的客户端的地址信息,若不想获取,则填NULL;
AF_INET: man 7 ip
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */ 必须填AF_INET
in_port_t sin_port; /* port in network byte order */ 端口号的网络字节序:1024~49151
struct in_addr sin_addr; /* internet address */ 本机IP地址的网络字节序。ifconfig
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
socklen_t *addrlen:真实的地址信息结构体大小,注意是指针类型。
返回值:
成功,返回新的文件描述符,该文件描述符才是通信使用的文件描述符;
失败,返回-1,更新errno;
recv
功能:通过套接字文件描述符从接收缓冲区读取数据;
原型:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
int sockfd:填accept函数获取到的新的文件描述符;
void *buf:存储获取到的数据;可以读取任意类型数据;
size_t len:指定要读取多少个字节;
int flags:
0:阻塞方式读取,若没有数据可读,则recv函数阻塞。当flags填0,完全等价于read函数
MSG_DONTWAIT:非阻塞方式,若没有数据可读,recv函数不阻塞,但是函数会运行失败;
返回值:
>0, 成功读取到的字节数;
=0, 在流式套接字中,对端关闭, udp中不会出现该情况。
=-1,函数运行失败;
recv(sockfd, buf, len, flags);
等价于
recvfrom(sockfd, buf, len, flags, NULL, NULL);
recv是否可以用其他函数替换? read recvfrom
send
功能:通过套接字文件描述符向指定的发送缓冲区发送数据;
原型:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
int sockfd:填accept函数获取到的新的文件描述符;
void *buf:存储获取到的数据;可以读取任意类型数据;
size_t len:指定要读取多少个字节;
int flags:
0:阻塞方式发送,若缓冲区满,则该函数阻塞;当flags填0,则完全等价于write.
MSG_DONTWAIT:非阻塞方式,若缓冲区满,则该函数不阻塞,但是函数会运行失败;
返回值:
>0, 成功发送的字节数;
=-1,函数运行失败;
send(sockfd, buf, len, flags);等价于sendto(sockfd, buf, len, flags, NULL, 0);
send是否可以用其他函数替换? write sendto
connect
功能:连接服务器;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
int sockfd:指定要连接到服务器的套接字文件描述符;
struct sockaddr *addr:通用地址信息结构体,真实的地址信息结构体根据地址族指定。
指定要连接的服务器的地址信息结构体;
socklen_t addrlen:真实的地址信息结构体的大小;
返回值:
成功,返回0;
失败,返回-1,更新errno;
代码示例
服务器
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_MSG(msg) do{\
fprintf(stderr, "__%s__ __%s__ __%d__ ", __FILE__, __func__, __LINE__);\
perror(msg);\
}while(0)
#define IP "192.168.50.103" //ifconfig
#define PORT 6666 //端口号 1024~49151
int main(int argc, const char *argv[])
{
//创建流式套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("socket success sfd=%d\n", sfd);
//允许端口快速被覆盖重用。
int reuse = 1;
if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
{
ERR_MSG("setsockopt");
return -1;
}
printf("允许端口快速被覆盖重用成功\n");
//填充地址信息结构体,真实的地址信息结构体AF_INET: man 7 IP
struct sockaddr_in sin;
sin.sin_family = AF_INET; //必须填AF_INET;
sin.sin_port = htons(PORT); //端口号的网络字节序1024~49151
sin.sin_addr.s_addr = inet_addr(IP);//本机IP地址的网络字节序
//绑定服务器的地址信息
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("bind");
return -1;
}
printf("bind success __%d__\n", __LINE__);
//将套接字设置为被动监听状态
if(listen(sfd, 128) < 0)
{
ERR_MSG("listen");
return -1;
}
printf("listen success __%d__\n", __LINE__);
struct sockaddr_in cin; //存储客户端地址信息
socklen_t addrlen = sizeof(cin);
//生成新的文件描述符与客户端通信
int newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen);
if(newfd < 0)
{
ERR_MSG("accept");
return -1;
}
printf("[%s:%d] newfd=%d 客户端连接成功 __%d__\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, __LINE__);
char buf[128] = "";
ssize_t res = 0;
while(1)
{
bzero(buf, sizeof(buf));
//接收
res = recv(newfd, buf, sizeof(buf), 0);
if(res < 0)
{
ERR_MSG("recv");
return -1;
}
else if(0 == res)
{
printf("[%s:%d] newfd=%d 客户端下线 __%d__\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, __LINE__);
break;
}
printf("[%s:%d] newfd=%d:%s __%d__\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, buf, __LINE__);
//发送
/*
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0;
*/
strcat(buf, "*_*");
if(send(newfd, buf, sizeof(buf), 0) < 0)
{
ERR_MSG("send");
return -1;
}
printf("send success __%d__\n", __LINE__);
}
//关闭
if(close(newfd) < 0)
{
ERR_MSG("close");
return -1;
}
if(close(sfd) < 0)
{
ERR_MSG("close");
return -1;
}
return 0;
}
客户端
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_MSG(msg) do{\
fprintf(stderr, "__%s__ __%s__ __%d__ ", __FILE__, __func__, __LINE__);\
perror(msg);\
}while(0)
#define IP "192.168.50.103" //ifconfig
#define PORT 6666 //端口号 1024~49151
int main(int argc, const char *argv[])
{
//创建流式套接字
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if(cfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("create socket success cfd=%d __%d__\n", cfd, __LINE__);
//绑定客户端自身的地址信息结构体---》非必须绑定
//若客户端没有绑定地址信息,
//则操作系统会自动帮客户端绑定本机IP,以及49151~65535内的随机端口
//填充服务器的地址信息结构体,给connect函数使用
//要连接哪个服务器,就填哪个服务器的地址信息
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT); //服务器的端口号
sin.sin_addr.s_addr = inet_addr(IP); //服务器的IP
//连接服务器
if(connect(cfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("connect");
return -1;
}
printf("connect success __%d__\n", __LINE__);
char buf[128] = "";
ssize_t res = 0;
while(1)
{
//从终端获取数据
printf("请输入>>> ");
fgets(buf,sizeof(buf), stdin);
buf[strlen(buf)-1] = 0;
//发送
if(send(cfd, buf, sizeof(buf), 0) < 0)
{
ERR_MSG("send");
return -1;
}
printf("send success __%d__\n", __LINE__);
//接收
bzero(buf, sizeof(buf)); //memset
res = recv(cfd, buf, sizeof(buf), 0);
if(res < 0)
{
ERR_MSG("recv");
return -1;
}
else if(0 == res)
{
printf("服务器离线\n");
break;
}
printf(":%s __%d__\n", buf, __LINE__);
}
//关闭
if(close(cfd) < 0)
{
ERR_MSG("close");
return -1;
}
return 0;
}
UDP编程
UDP流程图
UDP编程
socket
功能:在内核空间中创建接收缓冲区和发送缓冲区,并在用户空间获取到该缓冲区的文件描述符;
用户可以通过该文件描述符操作缓冲区;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
int 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)
int type:
SOCK_STREAM: 字节流式套接字,流式套接字,----》默认指定TCP协议
SOCK_DGRAM: 数据报式套接字,报式套接字,----> 默认指定UDP协议。
SOCK_RAW: 原始套接字,协议需要在第三个参数指定。
int protocol:0,使用默认协议。
IPPROTO_TCP IPPROTO_UDP
返回值:
成功,返回的套接字文件描述符 >0;
失败,返回-1,更新errno;
bind
功能:将地址信息结构体与套接字文件绑定;
原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
int sockfd:套接字文件描述符,其实就是socket函数的返回值;
struct sockaddr *addr:通用地址信息结构体,真实的地址信息结构体,根据地址族指定
需要手动填充要绑定到套接字上的IP和端口;
AF_INET: man 7 ip
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */ 必须填AF_INET;
in_port_t sin_port; /* port in network byte order */ 端口号的网络字节序,1024~49151
struct in_addr sin_addr; /* internet address */ 本机IP地址的网络字节序
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
socklen_t addrlen:真实的地址信息结构体的大小,sizeof(struct sockaddr_in)
返回值:
成功,返回0;
失败,返回-1,更新errno;
recvfrom
功能:从接收缓冲区中接收数据,同时可以获取到该数据包从哪里来,即发送方的地址信息。
原型:
#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);
参数:
int sockfd:填accept函数获取到的新的文件描述符;
void *buf:存储获取到的数据;可以读取任意类型数据;
size_t len:指定要读取多少个字节;
int flags:
0:阻塞方式读取,若没有数据可读,则recv函数阻塞。当flags填0,完全等价于read函数
MSG_DONTWAIT:非阻塞方式,若没有数据可读,recv函数不阻塞,但是函数会运行失败;
struct sockaddr *addr:通用地址信息结构体,真实的地址信息结构体根据地址族指定。
函数运行完毕后,会存储这个数据包从谁那里发送过来。即发送方的地址信息。若不想接收则填NULL;
socklen_t *addrlen:真实的地址信息结构体的大小; 注意是指针类型。
返回值:
>0, 成功读取到的字节数;
=0, 在流式套接字中,对端关闭
=-1,函数运行失败;
recv(sockfd, buf, len, flags);
等价于
recvfrom(sockfd, buf, len, flags, NULL, NULL);
sendto
功能:发送数据到指定接收方;
原型:
#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);
参数:
int sockfd:填accept函数获取到的新的文件描述符;
void *buf:存储获取到的数据;可以读取任意类型数据;
size_t len:指定要读取多少个字节;
int flags:
0:阻塞方式发送,若缓冲区满,则该函数阻塞;当flags填0,则完全等价于write.
MSG_DONTWAIT:非阻塞方式,若缓冲区满,则该函数不阻塞,但是函数会运行失败;
struct sockaddr *dest_addr:需要手动填充地址信息,表示该数据包应该发给谁。要发给谁就填谁的地址信息。
socklen_t addrlen:真实的地址信息结构体大小;
返回值:
>0, 成功发送的字节数;
=-1,函数运行失败;
send(sockfd, buf, len, flags);等价于sendto(sockfd, buf, len, flags, NULL, 0);
示例代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_MSG(msg) do{\
fprintf(stderr, "__%s__ __%s__ __%d__ ", __FILE__, __func__, __LINE__);\
perror(msg);\
}while(0)
#define PORT 6666 //1024~49151
#define IP "192.168.50.103" //ifconfig
int main(int argc, const char *argv[])
{
//创建报式套接字
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("sfd = %d __%d__\n", sfd, __LINE__);
//填充服务器的地址信息结构体, AF_INET: man 7 ip
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT); //1024~49151
sin.sin_addr.s_addr = inet_addr(IP); //ifconfig
//绑定服务器自身的地址信息
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("bind");
return -1;
}
printf("bind success __%d__\n", __LINE__);
char buf[128] = "";
struct sockaddr_in cin; //存储客户端的地址信息
socklen_t addrlen = sizeof(cin);
ssize_t res = 0;
while(1)
{
bzero(buf, sizeof(buf));
//接收
res = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &addrlen);
//res = recvfrom(sfd, buf, sizeof(buf), 0, NULL, NULL);
//res = recv(sfd, buf, sizeof(buf), 0);
if(res < 0)
{
ERR_MSG("recvfrom");
return -1;
}
printf("[%s:%d] : %s __%d__\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf, __LINE__);
//发送 --> 谁发给我,我发还给谁
strcat(buf, "*_*");
if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, sizeof(cin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
printf("sendto success __%d__\n", __LINE__);
}
//关闭
if(close(sfd) < 0)
{
ERR_MSG("close");
return -1;
}
return 0;
}
客户端
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_MSG(msg) do{\
fprintf(stderr, "__%s__ __%s__ __%d__ ", __FILE__, __func__, __LINE__);\
perror(msg);\
}while(0)
#define PORT 6666 //1024~49151
#define IP "192.168.50.103" //ifconfig
int main(int argc, const char *argv[])
{
//创建报式套接字
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if(cfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("cfd = %d __%d__\n", cfd, __LINE__);
//绑定客户端自身的地址信息---》非必须绑定
//若不绑定则操作系统会自动给客户端绑定上一个可用IP以及随机端口。
/*
//绑定服务器自身的地址信息
if(bind(cfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("bind");
return -1;
}
printf("bind success __%d__\n", __LINE__);
*/
//填充服务器的地址信息结构体,给sendto函数使用
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT); //服务器的端口
sin.sin_addr.s_addr = inet_addr(IP); //服务器的IP
char buf[128] = "";
struct sockaddr_in dstAddr;
socklen_t addrlen = sizeof(dstAddr);
ssize_t res = 0;
while(1)
{
bzero(buf, sizeof(buf));
printf("请输入>>> ");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0;
if(sendto(cfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
printf("sendto success __%d__\n", __LINE__);
//接收
res = recvfrom(cfd, buf, sizeof(buf), 0, (struct sockaddr*)&dstAddr, &addrlen);
//res = recvfrom(cfd, buf, sizeof(buf), 0, NULL, NULL);
//res = recv(cfd, buf, sizeof(buf), 0);
//res = read(cfd, buf, sizeof(buf));
if(res < 0)
{
ERR_MSG("recvfrom");
return -1;
}
printf("[%s:%d] : %s __%d__\n", \
inet_ntoa(dstAddr.sin_addr), ntohs(dstAddr.sin_port), buf, __LINE__);
}
//关闭
if(close(cfd) < 0)
{
ERR_MSG("close");
return -1;
}
return 0;
}
UDP中connect函数
udp中可以调用connect函数。
1. TCP中的connect函数会产生三次握手,将server和client连接起来。
UDP中的connect函数不会产生连接,UDP中的connect函数仅仅是将对端的IP地址和端口号填充到内核套接字中,此时UDP只能与记录的对端进行通信。
2. TCP的connect函数只能调用一次。
UDP中的connect函数可以调用多次,用于刷新内核中对端的地址信息。若想清空内核中对端的地址信息,可以将sin_family设置为AF_UNSPEC(sin_family=AF_UNSPEC)。
3. 当UDP采用connect的方式收发报文后,可以调用以下形式:
- sendto send write
- recvfrom recv read
使用recvfrom和sendto的时候,建议使用以下形式:
recvfrom(sfd,buf,sizeof(buf),0,NULL,NULL);
sendto(sfd,buf,sizeof(buf),0,NULL,0);
优点
1. 提高传输效率
不调用connect函数传输:
填充对端地址信息到内核中 --> 发送数据 --> 清空内核中对端的信息 --> 填充对端地址信息到内核中 --> 发送数据 --> 清空内核中的对端信息
除第一次外,每次发送信息都需要先清空内核中对端的信息再发送。
调用connect函数传输:
填充对端地址信息到内核中 -->发送数据 --> 发送数据 --> 发送数据 --> ...... --> 清空内核中的对端信息
客户端和服务端连接成功后,所有信息都发送完再清空内核中对端的信息。
2.增加传输的稳定性
防止AB两个进程在做大量数据传输的时候接收到C进程的数据,从而导致数据错乱。
问题:
1.UDP是否能用connect函数?
可以,并且需要将上述的内容一一阐述清楚
2.UDP中的recvfrom是否可以替换成其他函数?
可以,UDP采用connect的方式收发报文时可以将recvfrom替换成reccv函数或write函数
3.UDP中的sendto是否可以替换成其他函数?
可以,UDP采用connect的方式收发报文时可以将sendto替换成send函数或read函数
T1和T3问题其实一样
示例代码(服务端)
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_MSG(msg) do{\
fprintf(stderr, "__%s__ __%s__ __%d__ ", __FILE__, __func__, __LINE__);\
perror(msg);\
}while(0)
#define PORT 6666 //1024~49151
#define IP "192.168.50.103" //ifconfig
int main(int argc, const char *argv[])
{
//创建报式套接字
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("sfd = %d __%d__\n", sfd, __LINE__);
//填充服务器的地址信息结构体, AF_INET: man 7 ip
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT); //1024~49151
sin.sin_addr.s_addr = inet_addr(IP); //ifconfig
//绑定服务器自身的地址信息
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("bind");
return -1;
}
printf("bind success __%d__\n", __LINE__);
char buf[128] = "";
struct sockaddr_in cin; //存储客户端的地址信息
socklen_t addrlen = sizeof(cin);
ssize_t res = 0;
while(1)
{
bzero(buf, sizeof(buf));
//接收
res = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &addrlen);
if(res < 0)
{
ERR_MSG("recvfrom");
return -1;
}
printf("[%s:%d] : %s __%d__\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf, __LINE__);
//connect函数,将对端的地址信息填充到内核中
//调用connect函数,此时该udp只能与记录的对端进行通信
if(connect(sfd, (struct sockaddr*)&cin, sizeof(cin)) < 0)
{
ERR_MSG("connect");
return -1;
}
printf("udp connect [%s:%d] success __%d__\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), __LINE__);
while(1)
{
bzero(buf, sizeof(buf));
res = recvfrom(sfd, buf, sizeof(buf), 0, NULL, NULL);
if(res < 0)
{
ERR_MSG("recvfrom");
return -1;
}
printf("[%s:%d] : %s __%d__\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf, __LINE__);
if(strcmp(buf, "quit") == 0)
break;
//发送 --> 谁发给我,我发还给谁
strcat(buf, "*_*");
if(write(sfd, buf, sizeof(buf)) < 0)
{
ERR_MSG("sendto");
return -1;
}
printf("sendto success __%d__\n", __LINE__);
}
cin.sin_family = AF_UNSPEC; //清空内核中对端的地址信息
//调用connect函数,此时该udp只能与记录的对端进行通信
if(connect(sfd, (struct sockaddr*)&cin, sizeof(cin)) < 0)
{
ERR_MSG("connect");
return -1;
}
printf("udp connect AF_UNSPEC success\n");
}
//关闭
if(close(sfd) < 0)
{
ERR_MSG("close");
return -1;
}
return 0;
}
基于UDP的tftp协议
tftp协议概述
tftp是简单文件传输协议,适用于在网络上进行文件传输的一套标准协议,使用UDP传输。
特点:
- tftp是应停用层协议
- 基于UDP协议实现
数据传输模式:
- octet:二进制模式(常用)
- mail:已经不再支持
tftp通信过程总结
- 服务器在69号端口等待客户端的请求
- 服务器若批准请求,则使用临时端口与客户端进行通信
- 每个数据包的编号都有变化(从1开始)
- 每个数据包都要得到ACK的确认,如果出现超时,则需要重新发送最后的数据包或ACK包
- 数据长度以512Byte传输,小于512Byte的数据意味着数据传输结束
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 <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_MSG(msg) do{\
fprintf(stderr, "__%s__ __%s__ __%d__ ", __FILE__, __func__, __LINE__);\
perror(msg);\
}while(0)
#define PORT 69 //固定69号端口
#define IP "192.168.50.183" //windowsIP
int do_download(int cfd, struct sockaddr_in sin);
int main(int argc, const char *argv[])
{
//创建报式套接字
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if(cfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("cfd = %d __%d__\n", cfd, __LINE__);
//绑定客户端自身的地址信息---》非必须绑定
//若不绑定则操作系统会自动给客户端绑定上一个可用IP以及随机端口。
//填充服务器的地址信息结构体,给sendto函数使用
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT); //服务器的端口
sin.sin_addr.s_addr = inet_addr(IP); //服务器的IP
char choose = 0;
while(1)
{
printf("----------------------------\n");
printf("----------1. 下载-----------\n");
printf("----------2. 上传-----------\n");
printf("----------3. 退出-----------\n");
printf("----------------------------\n");
printf("请输入>>> ");
choose = getchar();
while(getchar()!=10);
switch(choose)
{
case '1':
do_download(cfd, sin);
break;
case '2':
//do_upload();
break;
case '3':
goto END;
default:
printf("输入错误,请重新输入\n");
}
}
END:
//关闭
if(close(cfd) < 0)
{
ERR_MSG("close");
return -1;
}
return 0;
}
int do_download(int cfd, struct sockaddr_in sin)
{
printf("请输入要下载的文件名>>> ");
char name[20] = "";
scanf("%s", name);
while(getchar()!=10);
//组下载请求包
char buf[516] = "";
int size = sprintf(buf, "%c%c%s%c%s%c", 0, 1, name, 0, "octet", 0);
if(sendto(cfd, buf, size, 0, (struct sockaddr*)&sin, sizeof(sin)) <0)
{
ERR_MSG("sendto");
return -1;
}
printf("sendto success\n");
//打开一个文件,以可写的方式
socklen_t addrlen = sizeof(sin);
ssize_t res = 0;
while(1)
{
bzero(buf, sizeof(buf));
//接收数据包
if((res=recvfrom(cfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, &addrlen)) < 0)
{
ERR_MSG("recvfrom");
return -1;
}
//判断是否是数据包 buf[1]==3 buf[1]==5
if(3 == buf[1])
{
//处理重复收包的情况,通过块编号判断是否重复
//存储数据到文件中
//write(fd, buf+4, res-4)
//回复ACK --->数据包和ACK包前4个字节,只有操作码不一致
//由于操作码是大端字节序,所以有效操作码其实存在buf[1]的位置
buf[1] = 4;
if(sendto(cfd, buf, 4, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
//当数据小于512的时候要结束循环,即整个数据包要小于516
if(res<512+2+2)
{
printf("下载完毕\n");
break;
}
}
else if(5 == buf[1])
{
}
}
//关闭文件
return 0;
}