Linux socket网络编程实现FTP服务器

        服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

 1.TCP和UDP是什么?

TCP:
        传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 定义。

UDP:
        Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。RFC 768 描述了 UDP。

区别:

       *TCP面向连接,通过三次握手建立连接,四次挥手接除连接;UDP是无连接的,即发送数据之前不需要建立连接,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功
       *TCP是
可靠的通信方式。通过TCP连接传送的数据,确保数据无差错,不丢失,不重复,且按序到达;而UDP由于无需连接的原因,将会以最大速度进行传输,但不保证可靠交付,也就是会出现丢失、重复等等问题。
       *TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流,由于连接的问题,当网络出现波动时,连接可能出现响应问题;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低。
       *每一条TCP连接
只能是点到点的;而UDP不建立连接,所以可以支持一对一,一对多,多对一和多对多的交互通信,也就是可以同时接受多个人的包。
       *TCP需要建立连接,首部开销20字节相比8个字节的UDP显得比较大。
       *TCP的逻辑通信信道是
全双工的可靠信道UDP则是不可靠信道

2.端口:

    端口的概念:

       在网络技术中,端口(Port)大致有两种意思:一是物理意义上的端口,比如,ADSL Modem、集线器、交换机、路由器用于连接其他网络设备的接口,如RJ-45端口、SC端口等等。二是逻辑意义上的端口,一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。
        我们这里将要介绍的就是逻辑意义上的端口。
我们这里所说的端口,不是计算机硬件的I/O端口,而是软件形式上的概念.工具提供服务类型的不同,端口分为两种,一种是TCP端口,一种是UDP端口。计算机之间相互通信的时候,分为两种方式:一种是发送信息以后,可以确认信息是否到达,也就是有应答的方式,这种方式大多采用TCP协议;一种是发送以后就不管了,不去确认信息是否到达,这种方式大多采用UDP协议。对应这两种协议的服务提供的端口,也就分为TCP端口和UDP端口。
   端口的作用:

       首先,tcp的连接是两个进程间的通信,端口号就是为了区分同一计算机上的不同进程,端口号本质上就是一个整型。


3.字节序:

       多字节数据存储在存储器中的顺序就叫做字节序。字节序又分为俩种,一种叫做小端字节序;另外一种叫做大端字节序

大端字节序:在大端字节序的机器中,首先会存储多字节数据类型的二进制表示的第一个字节;
小端字节序:在小端字节序的机器中,首先会存储多字节数据类型的二进制表示的最后一个字节;

*网络协议指定了通讯字节序—大端
*只有在多字节数据作为整体处理时才需要考虑字节序
*运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
*异构计算机之间通讯,需要转换自己的字节序为网络字节序

 简单的总结大小端存储方式:

        *Big Endian 是指低地址端 存放 高位字节。
        *Little Endian 是指低地址端 存放 低位字节。


4.字节序转换函数:

uint16_t htons(uint16_t hostint16);
功能:
    将16位主机字节序数据转换成网络字节序数据
参数:
    uint16_t:unsigned short int
    hostint16:待转换的16位主机字节序数据
返回值:
    成功:返回网络字节序的值
头文件:
    #include <arpa/inet.h>

uint16_t ntohs(uint16_t netint16);//将16位网络字节序数据转换成主机字节序数据

uint32_t htonl(uint32_t hostint32);//将32位主机字节序数据转换成网络字节序数据

uint32_t ntohl(uint32_t netint32);//将32位网络字节序数据转换成主机字节序数据

5.地址转换API:

int inet_aton(const char *cp, struct in_addr *inp);

/*inet_aton函数将网络主机地址cp从 IPv4 的点分十格式转换为二进制值(以网络字节序)并且把它保存在inp指针指向的结构体中。如果地址是合法的,那么inet_aton函数返回非0值,反之返回0值。*/

char *inet_ntoa(struct in_addr in);

 /*inet_ntoa函数将网络主机地址in转换为点分十格式的 IPv4 地址。该函数的返回值所指向的字符串驻留在静态内存中,后续调用将覆盖该缓冲区。*/

6.实现socket服务器的过程:

    *socket创建套接字:int socket(int protofamily, int type, int protocol);//返回描述符sockfd

  socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

      正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

  • protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET
  • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

  *为套接字添加信息(IP地址端口号)int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
  • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。
  • 如ipv4对应的是: 
    struct sockaddr_in {
        sa_family_t    sin_family; /* address family: AF_INET */
        in_port_t      sin_port;   /* port in network byte order */
        struct in_addr sin_addr;   /* internet address */
    };
    
    /* Internet address. */
    struct in_addr {
        uint32_t       s_addr;     /* address in network byte order */
    };
  • addrlen:对应的是地址的长度。

     *监听并建立网络连接

 **服务端调用 int listen(int sockfd, int backlog);函数监听客户端的连接:

        listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

 **客户端调用int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);函数建立与服务端的连接:

        connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。

 **服务端在listen()监听之后调用int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);函数接受conneet()的请求连接建立完成 

参数sockfd:就是上面解释中的监听套接字。

参数addr:一个const struct sockaddr *指针,指向存储客户端地址的结构体(这个结构体和bind()函数中的相同),如果对客户的地址不感兴趣,那么可以把这个值设置为NULL

参数len:用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。

如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。

注意:

      accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字

       监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)。

       连接套接字:accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。

        自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。

    *数据交互:read()、write()等函数

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto() 

ssize_t read(int fd, void *buf, size_t count);

ssize_t write(int fd, const void *buf, size_t count);

/*在服务端中参数fd是accept()返回的连接套接字,在客户端中fd是socket()返回的套接字,其他参数都和文件中的open(),read()相同。*/

   *关闭套接字:int close(int fd);

需要分别关闭监听套接字和连接套接字。
        FTP服务端代码:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include "config.h"
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

char *getDesDir(char *cmsg)
{
	char *p;
	p=strtok(cmsg," ");
	p=strtok(NULL," ");
	return p;
}

int get_cmd_type(char *cmd)
{
	if(!strcmp("ls",cmd))         return LS;
	if(!strcmp("quit",cmd))       return QUIT;
	if(!strcmp("pwd",cmd))        return PWD;
	if(strstr(cmd,"cd") != NULL)  return CD;
	if(strstr(cmd,"get") != NULL) return GET;
	if(strstr(cmd,"put") != NULL) return PUT; 

	return 100;		 
}

void msg_handler(struct Msg msg,int fd)
{
	char cmdBuf[1024]={0};
	int ret;
	char *file=NULL;
	int fdfile;
	FILE *r=NULL;

	printf("smd:%s\n",msg.cmd);//打印指令
	ret=get_cmd_type(msg.cmd);

	switch(ret){
		case LS:
		case PWD:
			msg.type=0;
			r=popen(msg.cmd,"r");//popen可以将执行的结果写入指针r指向的地址中
			fread(msg.cmd,sizeof(msg.cmd),1,r);//将r中的内容度到msg.cmd中
			write(fd,&msg,sizeof(msg));//将结构体msg发送给客户端
			break;
		case CD:
			msg.type=1;
			char *dir=getDesDir(msg.cmd);
			printf("dir:%s\n",dir);
			chdir(dir);  //调用chdir()函数移至此文件下

			break;
		case GET:
			file=getDesDir(msg.cmd);//找到要下载的文件名

			if(access(file,F_OK) == -1){ //access函数判断文件是否存在
				strcpy(msg.cmd,"No This File!");
				write(fd,&msg,sizeof(msg));//将提示信息发送给客户端
			}else{
				msg.type=DOFILE;

				fdfile=open(file,O_RDWR);//打开目标文件
				read(fdfile,cmdBuf,sizeof(cmdBuf));//将文件内容读到cmdBuf中

				strcpy(msg.cmd,cmdBuf);//复制文件
				write(fd,&msg,sizeof(msg));//将内容发送给客户端
			}
			break;
		case PUT:
			fdfile=open(getDesDir(msg.cmd),O_RDWR|O_CREAT|0666);
			write(fdfile,msg.secondBuf,strlen(msg.secondBuf));
			close(fdfile);
			break;
		case QUIT:
			printf("client quit!\n");
			exit(-1);
	}   
}

int main(int argc,char **argv)
{
	int s_fd;//监听套接字
	int c_fd;//连接套接字
	int n_read;
	int addrlen;

	struct sockaddr_in s_addr;//定义socket中的结构体
	struct sockaddr_in c_addr;//定义accept中的结构体
	struct Msg msg;//命令接收结构体

	memset(&s_addr,0,sizeof(struct sockaddr_in));
	memset(&c_addr,0,sizeof(struct sockaddr_in));

	if(argc != 3){
		printf("data error!");
		exit(-1);
	}

	s_fd=socket(AF_INET,SOCK_STREAM,0);//创建套接字并判断
	if(s_fd == -1){
		perror("socket:");
		exit(-1);
	}

	s_addr.sin_family=AF_INET;//确定TCP/IP协议
	s_addr.sin_port=htons(atoi(argv[2]));//将端口号转变为网络字节序
	inet_aton(argv[1],&s_addr.sin_addr);//将地址转变为网络字节序

	bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));//给套接字添加信息

	listen(s_fd,10);//监听,规定可以有10个客户端接入

	while(1){
		addrlen=sizeof(struct sockaddr_in);
		c_fd=accept(s_fd,(struct sockaddr *)&c_addr,&addrlen);//阻塞等待连接并返回连接套接字
		if(fork() == 0){ //进入子进程
			while(1){
				n_read=read(c_fd,&msg,sizeof(msg));//接收客户端发送的信息放入结构体msg中
				if(n_read == 0){
					printf("client out\n");
					break;
				}else{
					msg_handler(msg,c_fd);//调用命令处理函数
				}        
			}
		}
	}
	close(c_fd);
	close(s_fd);
	return 0;
}

    FTP客户端代码:

#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include "config.h"
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

char *getDesDir(char *cmsg)
{
     char *p;
     p=strtok(cmsg," ");
     p=strtok(NULL," ");
     return p;
}

int get_cmd_type(char *cmd)
{
     if(!strcmp("ls",cmd))         return LS;
     if(!strcmp("quit",cmd))       return QUIT;
     if(!strcmp("pwd",cmd))        return PWD;
     if(strstr(cmd,"cd") != NULL)  return CD;
     if(strstr(cmd,"get") != NULL) return GET;
     if(strstr(cmd,"put") != NULL) return PUT;
     if(!strcmp("lls",cmd))         return LLS;
     if(strstr(cmd,"lcd") != NULL)         return LCD;

     return -1;
}

int cmd_handler(struct Msg msg,int fd)
{
      char *dir=NULL;
      char buf[32];
      int ret;
      int filefd;

      ret=get_cmd_type(msg.cmd);

      switch(ret){
              case LS:
              case CD:
              case PWD:
                     msg.type=0;
                     write(fd,&msg,sizeof(msg));
                     break;
              case GET:

                     msg.type=2;
                     write(fd,&msg,sizeof(msg));
                     break;
              case PUT:
                     strcpy(buf,msg.cmd);
                     dir=getDesDir(buf);

                     if(access(dir,F_OK) == -1){
                              printf("%s not exsit\n",dir);   
                     }else{
                              filefd=open(dir,O_RDWR);
                              read(filefd,msg.secondBuf,sizeof(msg.secondBuf));
                              close(filefd);

                              write(fd,&msg,sizeof(msg)); 
                     } 
                     break;
              case LLS:
                     system("ls");
                     break;
              case LCD:
                     dir=getDesDir(msg.cmd);
                     chdir(dir);
                     break;
              case QUIT:
                     strcpy(msg.cmd,"quit");
                     write(fd,&msg,sizeof(msg));
                     close(fd);
                     exit(-1);
      }
      return ret;
}

void handler_server_message(int fd,struct Msg msg)
{
      int n_read;
      int new_file_fd;
      struct Msg msg_get;
      n_read=read(fd,&msg_get,sizeof(msg_get));//从服务器接收信息
      
      if(n_read == 0){
               printf("server is out,quit\n");
               exit(-1); 
      }

      if(msg_get.type == DOFILE){
               char *p=getDesDir(msg.cmd);
               new_file_fd=open(p,O_RDWR|O_CREAT|0600);
               write(new_file_fd,msg_get.cmd,strlen(msg_get.cmd));
               putchar('>');
               close(new_file_fd);
               fflush(stdout);
      }else{
               printf("................................\n");
               printf("\n%s\n",msg_get.cmd);
               printf("................................\n");

               putchar('>');
               fflush(stdout);
      }
}

int main(int argc,char **argv)
{
      int c_fd;
      
      struct sockaddr_in c_addr;//客户端不需要bind,listen,accept,只用一个套接字
      struct Msg msg;

      if(argc != 3){
              printf("data error\n");
              exit(-1);
      }

      memset(&c_addr,0,sizeof(struct sockaddr_in));
      memset(&msg,0,sizeof(msg));

      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_in)) == -1){
              perror("connect");//申请连接
      }else{
              printf("connect ....\n");  
      } 

      int mark=0;
      while(1){
              memset(msg.cmd,0,sizeof(msg.cmd));
              if(mark == 0){
                     printf(">");
              }

              gets(msg.cmd);//获取指令

              if(strlen(msg.cmd) == 0){
                    if(mark == 1){
                            printf(">");
                    }
                    continue;      
              }

              mark=1;

              int ret=cmd_handler(msg,c_fd);//调用指令函数

              if(ret>IFGO){
                    putchar('>');
                    fflush(stdout);
                    continue;
              }
              if(ret == -1){
                    printf("command not\n");
                    fflush(stdout);
                    continue;
              }
              handler_server_message(c_fd,msg);
      }
      return 0;
}

     头文件:

#define LS   0
#define GET  1
#define PWD  2
#define CD   3

#define IFGO 4

#define LCD  5
#define LLS  6
#define PUT  7

#define QUIT 8
#define DOFILE 9


struct Msg
{
        int type;
        char cmd[1024];
        char secondBuf[128];
};

  • 8
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux中使用Socket网络编程实现FTP的put上传功能,可以通过以下步骤来实现: 1. 导入必要的头文件,包括sys/socket.h、netinet/in.h和arpa/inet.h,以及stdio.h、string.h和unistd.h。 2. 创建客户端Socket,使用函数socket()创建一个套接字。套接字使用AF_INET作为地址族,并使用SOCK_STREAM作为套接字类型。 3. 使用函数connect()连接到服务器FTP端口。为此,需要定义服务器IP地址和端口号,并将其传递给connect()函数。 4. 获取要上传的文件的文件名,并使用open()函数打开该文件。如果文件打开失败,则发送错误消息到服务器,并退出程序。 5. 使用send()函数将要上传的文件名发送到服务器。 6. 使用read()函数从文件中读取数据,并使用send()函数将数据发送到服务器。发送的数据可以使用固定大小的缓冲区。 7. 当文件读取完成后,发送一个结束标志,并关闭文件。这应该在循环中完成,直到文件被完全上传。 8. 使用recv()函数接收从服务器返回的响应,并根据响应进行相应的处理。例如,如果服务器返回成功的响应,表示文件上传成功。 9. 最后,使用close()函数关闭套接字连接,并释放所有资源。 上述是使用Socket网络编程Linux实现FTP的put上传的基本步骤。需要注意的是,FTP协议本身是非常复杂的,还需要考虑进行用户身份验证、传输模式选择等更多功能的实现。这只是一个简单的上传示例,更完整和高级的FTP上传功能可能需要更多的代码和功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值