选修了一门TCP/IP协议分析及应用,做了几个小实验,这里把实验的原理、自己的代码和经验教训和大家分享一下。
实验中的全部代码托管在Github上,请通过 fork + pull request 方法来帮助改进项目。
UDP协议是User Datagram Protocol 的简称,他是无连接的,不可靠的网络协议。
本实验目的是使用因特网提供的UDP传输协议,实现一个简单的UDP客户/服务器程序,以了解传输层所提供的UDP服务的特点,应用层和传输层之间的软件接口风格,熟悉socket机制和UDP客户端/服务器方式程序的结构。
本文在介绍UDP协议收发技术的同时,提供了相关代码,并将笔者在debug过程中的经验和教训带给读者。
设计与实现UDP echo客户及服务器程序,完成以下功能:
客户从标准输入读一行文本,写到服务器上;服务器从网络输入读取此行,并回射(echo)给客户;客户读此回射行,并将其写到标准输出。
扩展一下三个内容
* 在客户机上显示服务器的目录
* 将服务器指定的文件下载到客户机
* 讲客户机指定的文件上传到服务器
使用UDP进行程序设计可以分为客户端和服务器端两个部分。服务器端主要包含建立套接字、将套接字与地址结构进行帮顶、读写数据、关闭套接字几个过程。客户端包括建立套接字、读写数据、关闭套接字几个过程。服务器端和客户端两个流程志坚的主要差别在于对地址的绑定(bind()
)函数,客户端可以不用进行地址和端口的绑定操作。
上图中对UDP协议服务器程序框架进行了说明,服务器流程主要分为下述6个部分吗,即建立套接字、设置套接字地址参数、进行端口绑定、接收数据、发送数据、关闭套接字。
1. 建立套接字文件描述符,使用socket()
, 生成套接字文件描述符,例如:
int s = socket(AF_INET, SOCK_DGRAM, 0);
建立一个AF_INET
族的数据包套接字,UDP协议的套接字使用SOCK_DGRAM
选项。
2. 设置服务器地址和侦听端口,初始化要绑定的网络地址结构,例如:
struct sockaddr addr_serv;
addr_serv.sin_family = AF_INET; //地址类型为AF_INET
addr_serv.sin.addr.s_addr = htonl(INADDR_ANY); //任意本机地址
addr_serv.sin_port = htons(PORT_SERV); //服务器端口
地址结构的类型为AF_INEF
; IP地址为任意的本地地址; 服务器的端口为用户定义的端口地址; 注意成员sin_addr.s_addr
和sin_port
均为网络字节序。
3. 绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定,例如
bind(s,(struct sockaddr*) &addr_serv,sizeof(addr_serv)); //绑定地址
recvfrom()
函数接受客户端的网络数据。sendto()
函数向服务器主机发送数据。close()
函数释放资源在上图中,同样对UDP协议的客户端流程进行了描述,按照图中所示,UDP协议的客户端流程分为套接字建立、设置目的的地址和端口、向服务器发送数据、从服务器接收数据、关闭套接字5个部分。与服务器的框架相比,少了bind()
部分,客户端程序的端口和本地的地址可以由系统使用时指定,在使用sendto()
和recvfrom()
的时候,网络协议栈会临时指定本地的端口和地址,流程如下:
1. 建立套接字文件描述符,socket()
;
2. 设置服务器地址和端口,struct sockaddr
;
3. 向服务器地址和端口,sendto()
;
4. 接收服务器的数据,recvfrom()
;
5. 关闭套接字,close()
;
UDP协议常用的函数有recv()
/recvform()
、 send()
/ sendto()
、 socket()
、 bind()
等。当然这些函数同样可以用于TCP协议的程序设计。
UDP协议使用建立套接字的方式和TCP方式一样,使用socket()
函数,只不过协议的类型描述符使用SOCK_DGRAM
, 而不是参数SOCK_STREAM
。 例如下面是一个建立UDP套接字文件描述符的代码。
int s;
s = socket(AF_INEF, SOCK_DGRAM, 0);
UDP协议使用bind()
函数的方法与TCP没有什么差别,将一个套接字描述符与一个地址结构绑定在一起。例如,下面的代码将一个本地的地址和套接字文件描述符绑定在在了一起。
struct sockaddr_in local; //本地的地址信息
int from_len = sizeof(from); //地址结构的长度
local. sin _family = AF_INET; //协议簇
local. sin _port = htons(8888); //本地端口
local. sin _addr.s_addr = htonl(INADDR_ANY); //任意本机地址
s = socket(AF_INET, SOCK_DGRAM, 0); //初始化一个IPv4族的数据包套接字
if (s == -1) { //检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
bind(s, (struct sockaddr*) &local, sizeof(local)); //套接字绑定
绑定函数bind()
使用的时机,即什么时候需要绑定需要介绍一下。函数bind()
的作用是将一个套接字文件描述符和一个本地地址绑定在一起,即把发送数据的端口地址和IP地址进行了制定。例如在发送数据的时候,如果不进行绑定,则会临时选择一个随机的端口。
当客户端成功建立了一个套接字文件描述符并建立了合适的strcut sockaddr
结构后,或者服务器端成功将套接字文件描述符和地址结构绑定后,可以使用recv()
故意整个recvfrom()
来接收到达此套接字文件描述符上的数据或者在这个套接字文件描述符上的等待数据的到来。
recv()
函数和recvfrom()
函数的原型如下:
#include
#include
ssize_t recv(int s, void *buff, size_t len, int flags);
ssize_t recvfrom(int s, void *buff,size_t len, int flags, struct sockaddr *from, socklen_t *from)
当客户端成功建立了一个套接字文件描述符,并构建了合适的struct sockaddr
结构后,或者服务器成功地将套接字文件描述符和地址结构绑定后,可以使用send()
或者sendto()
函数来发送数据到某个主机上。
send()
函数和sendto()
函数的原型如下:
#include
#include
ssize_t send(int s, const void *buff, size_t len, int flags);
ssize_t sento(int s, const void *buff, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
dupcli01.cpp
/*
* udpcli01.cpp
*
* Created on: 2015年4月23日
* Author: gzxultra
*/
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 4096
#define LISTENQ 1024 /* 2nd argument to listen() */
#define SERV_PORT 9877
#define SA struct sockaddr
void dg_cli(FILE *, int, const SA *, socklen_t);
int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2) {
printf("usage:udpcli01sigio \n" );
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0))<0) {
printf("socket error.\n");
exit(1);
}
dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr));
exit(0);
}
void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
while (fgets(sendline, MAXLINE,fp) != NULL) {
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; /* null terminate */
fputs(recvline, stdout);
}
}
udpserv01.cpp
* 这里以亲身经历提醒各位同学,学习阶段就不要怕麻烦,规范的编程编译习惯受益终身。
*另外感谢v2ex的carto同学指出的错误,客户端传给服务端的字符串不带0,服务端取长度前要加0。
/*
* udpserv01.cpp
*
* Created on: 2015年4月23日
* Author: gzxultra
*/
#include
#include
#include
#include
#include
#include
#define MAXLINE 4096
#define LISTENQ 1024 /* 2nd argument to listen() */
#define SERV_PORT 9877
#define SA struct sockaddr
#include
static int sockfd;
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg);
void GetList(int sockfd, SA *pcliaddr, socklen_t clilen) ;
int main(int argc, char ** argv) {
struct sockaddr_in servaddr, cliaddr;
//使用socket()函数生成套接字文件描述符
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
printf("socket error.\n");
exit(1);
}
//设置服务器地址和侦听端口,初始化要绑定的网络地址结构
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET; //地址类型为AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意本地地址
servaddr.sin_port = htons(SERV_PORT); //服务器端口
//绑定侦听端口
bind(sockfd, (SA *) &servaddr, sizeof(servaddr));
char instruction[3][10] = {
"getlist\n",
"download\n",
"upload\n"
};
int op = 0;
int i = 0,j = 0;
int n = 0;
char mesg[MAXLINE];
socklen_t len = sizeof(cliaddr);
//printf("running here!\n");
while(1){
//len = clilen;
memset(mesg,0,sizeof(mesg));
//recvfrom接收客户端的网络数据
n = recvfrom(sockfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len);
op = 0;
//处理接受到的回车符
for(j=0;j<=n;j++){
if(mesg[j] == '\n') mesg[j] = '\0';
break;
}
for(i = 1;i<=3;i++)
if(strcmp(mesg,instruction[i-1])== 0){
op = i;
break;
} //choose operation
for(j=0;j<=n;j++){
if(mesg[j] == '\0') mesg[j] = '\n';
break;
}
printf("op = %d\n",op);
switch(op){
case 1:
GetList(sockfd, (SA *) &cliaddr, sizeof(cliaddr));
break;
//case 2:DownLoad();break;
//case 3:UpLoad();break;
default:{
dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr), mesg);
break;
}
}
//dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr), mesg);
//printf("echo done!\n");
}
}
void GetList(int sockfd, SA *pcliaddr, socklen_t clilen) {
int n;
socklen_t len;
printf("catch\n");
//char mesg[MAXLINE];
char strlin[100][MAXLINE]; //一百行缓冲区
char sendline[MAXLINE]={}; //最终发送的字符串
int i = 0;
FILE *fp;
system("ls -l>filelog.txt"); //输出重定向
//printf("2done!\n");
ssize_t read;
//char c;
//int count=0;
fp = fopen("filelog.txt", "r");
if (!fp) {
printf("get ls order failed!");
} else {
//printf("4done!\n");
//printf("%p\n", fp);
while (1) {
//printf("2done!!!!\n");
if (NULL==fgets(strlin[i], MAXLINE, fp))
break; //如果读到文件尾,结束读取
//strlin[i][strlen(strlin[i] - 1)] = '\0'; //每行加\0结尾
i++; //写成strlin[i++]提示i未定义,gcc的bug??
}
/*
++i;
memcpy(strlin[i],"EOF",sizeof("EOF"));
*/
for (int n = 0; n < i; n++){
strcat(sendline, strlin[n]);
//strcat(sendline,"\n");
}
//sendto(sockfd, strlin, sizeof(strlin), 0, pcliaddr, len);
fputs(sendline,stdout);
sendto(sockfd, sendline, strlen(sendline)+1, 0, pcliaddr, clilen);
}
//strcat(sendline, '\0');
fclose(fp);
printf("FileList Get!\n");
}
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg) {
//fputs(mesg,stdout);
//printf("\n");
sendto(sockfd, mesg, strlen(mesg)+1, 0, pcliaddr, clilen);
}
题目有三个扩展功能需要实现
1. 获取服务器的目录
2. 从服务器下载指定文件到客户机
3. 从客户机上载指定文件到服务器
三个扩展题的实现思路本质上是一样的,在服务器/客户机打开一个文档,按行读到尾部封装到套接字里一起发送,客户机/服务器收到后按行写入一个文件里。
其中第一个题目需要将system("ls -l")
的输出重定向到本地一个文本文件里,再读取发送。
附上完整的实现代码。
/*
* udpcli01.cpp
*
* Created on: 2015年4月23日
* Author: gzxultra
* Finished on: 2015年4月30日
*/
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 4096
#define LISTENQ 1024 /* 2nd argument to listen() */
#define SERV_PORT 9877
#define SA struct sockaddr
void dg_cli(FILE *, int, const SA *, socklen_t);
int main(int argc, char **argv) {
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2) {
printf("usage:udpcli01sigio \n" );
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
printf("socket error.\n");
exit(1);
}
dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));
exit(0);
}
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
char instruction[3][10] = {
"getlist\n",
"download\n",
"upload\n"
};
int i=0;
int op =0;
FILE *CopyToLocal;
FILE *Uploadfp;
while (fgets(sendline, MAXLINE,fp) != NULL) {
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; /* null terminate */
//fputs(recvline,stdout);
op = 0;
for(i = 1;i<=3;i++)
if(strcmp(sendline,instruction[i-1])== 0) {
op = i;
break;
} //choose operation
printf("op=%d\n",op);
switch(op) {
case 1: {
fputs(recvline, stdout);
break;
}
case 2: {
fputs(recvline, stdout);
fgets(sendline, MAXLINE,fp); //给出上传文件的文件名
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
memset(recvline,0,sizeof(recvline));
n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; /* null terminate */
char path[MAXLINE] = "/Users/gzxultra/downloads/";
strcat(path,sendline);
fputs(path,stdout);
printf("\n");
CopyToLocal = fopen(path,"w");
if(!CopyToLocal)
printf("download file failed!");
//fputs(recvline,stdout);
fputs(recvline, CopyToLocal);
//fputs(recvline,stdout);
//printf("download !");
fclose(CopyToLocal);
break;
}
case 3: {
fputs(recvline, stdout); //应该是提示输入文件名
fgets(sendline, MAXLINE,fp);//应该是输入要上传的文件名字吧?
sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
memset(recvline,0,sizeof(recvline));
//recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); //接收一個okay
//recvline[n] = 0;// null terminate
//fputs(sendline,stdout);
//fputs("\n",stdout);
for(int j=0;j<=n;j++) {
if(sendline[j] == '\n') sendline[j] = '\0';
//break;
}
//fputs(sendline,stdout);
//printf("\n");
Uploadfp = fopen(sendline, "r");
int i =0;
char strlin[1000][MAXLINE];
//printf("don't\n");
if (!Uploadfp) {
printf("get file failed!\n");
} else {
//printf("4done!\n");
//printf("%p\n", fp);
while (1) {
//printf("2done!!!!\n");
if (NULL==fgets(strlin[i], MAXLINE, Uploadfp))
break;//如果读到文件尾,结束读取
//strlin[i][strlen(strlin[i] - 1)] = '\0'; //每行加\0结尾
i++;//写成strlin[i++]提示i未定义,gcc的bug??
}
memset(sendline,0,sizeof(sendline));
for (int n = 0; n < i; n++) {
strcat(sendline, strlin[n]);
//strcat(sendline,"\n");
}
//sendto(sockfd, strlin, sizeof(strlin), 0, pcliaddr, len);
//fputs(sendline,stdout);
sendto(sockfd, sendline, strlen(sendline)+1, 0, pservaddr, servlen);
//recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
//fputs(recvline,stdout);//path
//fputs("\n",stdout);
}
//strcat(sendline, '\0');
fclose(Uploadfp);
}
default: {
fputs(recvline, stdout);
break;
}
//to spilit up the output by ***
}
for(int i=0;i<25;i++) {
printf("*");
}
printf("\n");
}
}
/*
* udpserv01.cpp
*
* Created on: 2015年4月23日
* Author: gzxultra
* Finished on: 2015年4月30日
*/
#include
#include
#include
#include
#include
#include
#define MAXLINE 4096
#define LISTENQ 1024 /* 2nd argument to listen() */
#define SERV_PORT 9877
#define SA struct sockaddr
#include
static int sockfd;
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg);
void GetList(int sockfd, SA *pcliaddr, socklen_t clilen);
void DownLoad(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg);
void UpLoad(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg);
int main(int argc, char ** argv) {
struct sockaddr_in servaddr, cliaddr;
//使用socket()函数生成套接字文件描述符
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
printf("socket error.\n");
exit(1);
}
//设置服务器地址和侦听端口,初始化要绑定的网络地址结构
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET; //地址类型为AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意本地地址
servaddr.sin_port = htons(SERV_PORT); //服务器端口
//绑定侦听端口
bind(sockfd, (SA *) &servaddr, sizeof(servaddr));
char instruction[3][10] = { "getlist\n", "download\n", "upload\n" };
int op = 0;
int i = 0, j = 0;
int n = 0;
char mesg[MAXLINE];
socklen_t len = sizeof(cliaddr);
//printf("running here!\n");
while (1) {
//len = clilen;
memset(mesg, 0, sizeof(mesg));
//recvfrom接收客户端的网络数据
n = recvfrom(sockfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len);
op = 0;
//处理接受到的回车符
for (i = 1; i <= 3; i++)
if (strcmp(mesg, instruction[i - 1]) == 0) {
op = i;
break;
} //choose operation
printf("op = %d\n", op);
switch (op) {
case 1:{
GetList(sockfd, (SA *) &cliaddr, sizeof(cliaddr));
break;
}
case 2: {
DownLoad(sockfd, (SA *) &cliaddr, sizeof(cliaddr), mesg);
break;
}
case 3: {
UpLoad(sockfd, (SA *) &cliaddr, sizeof(cliaddr), mesg);
break;
}
default: {
dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr), mesg);
break;
}
}//switch(op)
}//while
}//main
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg) {
//fputs(mesg,stdout);
//printf("\n");
sendto(sockfd, mesg, strlen(mesg) + 1, 0, pcliaddr, clilen);
}
void GetList(int sockfd, SA *pcliaddr, socklen_t clilen) {
int n;
socklen_t len;
printf("catch\n");
char strlin[100][MAXLINE]; //一百行缓冲区
char sendline[MAXLINE] = { }; //最终发送的字符串
int i = 0;
FILE *fp;
system("ls -l>filelog.txt"); //输出重定向
ssize_t read;
fp = fopen("filelog.txt", "r");
if (!fp) {
printf("get ls order failed!");
} else {
while (1) {
//printf("2done!!!!\n");
if (NULL == fgets(strlin[i], MAXLINE, fp))
break; //如果读到文件尾,结束读取
//strlin[i][strlen(strlin[i] - 1)] = '\0'; //每行加\0结尾
i++; //写成strlin[i++]提示i未定义,gcc的bug??
}
for (int n = 0; n < i; n++) {
strcat(sendline, strlin[n]);
//strcat(sendline,"\n");
}
//sendto(sockfd, strlin, sizeof(strlin), 0, pcliaddr, len);
fputs(sendline, stdout);
sendto(sockfd, sendline, strlen(sendline) + 1, 0, pcliaddr, clilen);
}
//strcat(sendline, '\0');
fclose(fp);
printf("FileList Get!\n");
}
void DownLoad(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg) {
char text[MAXLINE] = "Please input the file name you'd like to download\n";
char sendline[100 * MAXLINE];
char instr[] = "download\n";
int n = 0;
//fputs(mesg,stdout);
while (strcmp(mesg, instr) == 0) {
printf("start download\n");
sendto(sockfd, text, strlen(text) + 1, 0, pcliaddr, clilen);
n = recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &clilen);
}
FILE *Downloadfp;
fputs(mesg, stdout);
//fputs("\n",stdout);
for (int j = 0; j <= n; j++) {
if (mesg[j] == '\n')
mesg[j] = '\0';
//break;
}
Downloadfp = fopen(mesg, "r");
int i = 0;
char strlin[1000][MAXLINE];
//printf("don't\n");
if (!Downloadfp) {
printf("get file failed!\n");
} else {
while (1) {
//printf("2done!!!!\n");
if (NULL == fgets(strlin[i], MAXLINE, Downloadfp))
break; //如果读到文件尾,结束读取
//strlin[i][strlen(strlin[i] - 1)] = '\0'; //每行加\0结尾
i++; //写成strlin[i++]提示i未定义,gcc的bug??
}
memset(sendline, 0, sizeof(sendline));
for (int n = 0; n < i; n++) {
strcat(sendline, strlin[n]);
//strcat(sendline,"\n");
}
//sendto(sockfd, strlin, sizeof(strlin), 0, pcliaddr, len);
fputs(sendline, stdout);
sendto(sockfd, sendline, strlen(sendline) + 1, 0, pcliaddr, clilen);
}
//strcat(sendline, '\0');
fclose(Downloadfp);
printf("file download!\n");
}
void UpLoad(int sockfd, SA *pcliaddr, socklen_t clilen, char *mesg) {
char text[MAXLINE] = "Please input the file name you'd like to upload\n";
char recvline[100 * MAXLINE];
char instr[] = "upload\n";
int n = 0;
//fputs(mesg,stdout);
FILE *CopyToServer;
while (strcmp(mesg, instr) == 0) {
printf("start upload\n");
sendto(sockfd, text, strlen(text) + 1, 0, pcliaddr, clilen);
n = recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &clilen);
} //上面应该没问题了~
char path[MAXLINE] = "/Users/gzxultra/";
strcat(path, mesg);
CopyToServer = fopen(path, "w");
if (!CopyToServer)
printf("Upload file failed!\n");
else {
recvfrom(sockfd, recvline, 100 * MAXLINE, 0, pcliaddr, &clilen);
fputs(recvline, CopyToServer);
//sendto(sockfd, path, strlen(path) + 1, 0, pcliaddr, clilen);
}
//fputs(recvline,stdout);
//fputs(recvline,stdout);
//printf("download !");
fclose(CopyToServer);
printf("file Uploadload!\n");
}
执行程序,
* 输入getlist获取服务器的目录
* 下载服务器文件filelog.txt
* 上载客户机文件daytimecli.c