服务器端先初始化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
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函数的三个参数分别为:
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
*为套接字添加信息(IP地址端口号)int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
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 */
};
*监听并建立网络连接
**服务端调用 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()等函数
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
#include /* See NOTES */
#include
#include
#include
#include
#include
#include "config.h"
#include
#include
#include
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
#include
#include
#include
#include "config.h"
#include
#include
#include
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];
};