基于TCP协议的接收和发送实验
选修了一门TCP/IP协议分析及应用,做了几个小实验,这里把实验的原理、自己的代码和经验教训和大家分享一下。
实验中的全部代码托管在Github上,请通过 fork + pull request 方法来帮助改进项目。
导语:
TCP协议是TCP/IP协议中很重要的一个协议,由于他传输的稳定性,在很多程序中都在使用,例如HTTP、FTP等协议都是在TCP的基础上进行构建的。
本实验目的是使用因特网提供的TCP传输协议,实现一个简单的UDP客户/服务器程序,以了解传输层所提供的TCP服务的特点,应用层和传输层之间的软件接口风格,熟悉socket机制和TCP客户端/服务器方式程序的结构。
本文在介绍TCP协议收发技术的同时,提供了相关代码,并将笔者在debug过程中的经验和教训带给读者。
一,实验内容
设计与实现一个简单的TCP echo客户/服务器程序,完成以下功能:
客户从标准输入读一行文本,写到服务器上;服务器从网络输入读取此行,并回射(echo)给客户;客户读此回射行,并将其写到标准输出。
扩展以下三个内容
* 在客户机上显示服务器的目录
* 将服务器指定的文件下载到客户机
* 讲客户机指定的文件上传到服务器
二,套接字编程基础
2.1 套接字地址结构
进行套接字编程需要要指定套接字的地址作为参数,不同的协议族有不同的地址结构定义方式。这些地址结构通常以sockaddr_
开头,每一个协议族有一个唯一的后缀,例如对于以太网,其结构名称为sockaddr_in
。
- 通用套接字数据结构
通用的套接字地址类型的定义如下,它可以在不同协议族之间进行强制转换。
struct sockaddr{ //套接字地址结构
sa_family_t sa_family; //协议族
char sa_data[14]; //协议族数据
}
上述结构中协议族成员变量sa_family的类型为sa_family_t,其实这个类型是unsigned short类型,因此成员变量sa_family的长度为16个字节。
typedef unsigned short sa_family_t;
- 实际使用的套接字数据结构
在网络程序设计中所使用的函数中几乎所有的套接字函数都用这个结构作为参数,如bind()
函数的原型为:
int bind(int sockfd, //套接字文件描述符
const struct sockaddr *my_addr, //套接字地址结构
socklen_t addrlen); //套接字地址结构的长度
但是使用struct sockaddr
不方便设置,在以太网中,一般军用结构struct sockaddr_in
进行设置,这个结构的定义如下:
struct sockaddr_in{ //以太网套接字结地址结构
u8 sin_len; //结构`struct sockaddr_in`的长度,16
u8 sin_family; //通常为AF_INET
u16 sin_port; //16位的端口号,网络字节序
sturct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //未用
};
结构struct sockaddr_in
的成员变量in_addr
用于标示IP地址,这个结构的定义如下
struct in_addr{ //IP地址结构
u32 s_addr; //32位IP地址,网络字节序
}
三,TCP网络编程流程
TCP网络编程框架
TCP网络编程有服务器模式和客户端模式两种。服务器模式创建一个服务程序,等待客户端用户的链接,接收到用户的请求后,根据用户的请求进行处理;用户端模式则根据服务器的地址和端口进行解析,向服务器发送请求并对服务器的响应进行数据处理。
服务端的程序设计模式
图为TCP链接的服务器模式的程序设计流程。流程主要为套接字初始化(socket()
),套接字与端口的绑定(bind()
),接收和发送数据(read()
、write()
)进行数据处理和处理完毕的套接字关闭(close()
)。- 套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项,这个过程中的函数为
socket()
,他按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。 - 套接字与端口绑定过程中,将套接字与一个地址结果进行绑定。绑定之后,在进行网络程序设计的时候,套接字所表示的IP地址和端口地址以及协议类型等参数按照帮定制进行操作。
- 由于一个服务器需要满足多个客户端的链接请求,而服务器在某个时间仅能处理有限个数的客户端的请求,所以服务器需要设置服务端排队队列的长度。服务器侦听链接会设置这个参数,限制客户端中等待服务器处理请求连接的队列长度。
- 在客户端发送连接请求后,服务器需要接受客户端的链接,然后才能进行其他的处理。
- 在服务器接受客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。
- 当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接。
- 套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项,这个过程中的函数为
客户端的程序设计模式
图中客户端设计模式,主要分为套接字初始化(socket()
),连接服务器((connnect()
),读写网络数据(read()
、write()
)并进行数据处理和最后的套接字关闭(clone()
)过程。
客户端程序设计模式流程与服务器端的处理模式流程类似,两者之间的不同之处是客户端在套接字初始化之后可以不进行地址绑定,而是直接连接服务器端。
客户端链接服务器的过程中,客户端根据用户设置的服务器地址、端口等参数特定的服务器程序进行通信。客户端与服务武器的交互过程
客户端与服务器在链接、读写数据、关闭连接过程中有交互的过程- 客户端的连接过程,对服务器是接受过程,在这个过程中客户端与服务器进行三次握手,建立TCP链接。建立TCP连接之后,客户端与服务器之间可以进行数据的交互。
- 客户端与服务器之间的数据交互是相对的过程,客户端的读数据过程对应了服务器端的写数据过程,客户端的写数据过程对应服务器的读数据的过程。
- 在服务器和客户端的数据交互完毕后,关闭套接字链接。
四,TCP协议程序设计的常用函数
4.1 创建网络插口函数socket()
网络程序设计中的套接字系统调用socket()
函数用来获得文件描述符
socket()
函数的原型如下,这个函数建立一个协议族为domain
、协议类型为type
、协议编号为protocol
的套接字文件描述符。如果函数调用成功,会返回一个表示这个套接字的文件描述符,失败的时候返回-1。
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
4.2 绑定一个地址端口对bind()
在建立套接字文件描述符成功后,需要对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作
bind()
函数将长度为addlen
的struct sockadd
类型的参数my_addr
与sockfd
绑定在一起。将sockfd
绑定到某个端口上,如果使用connect()
函数则没有绑定的必要。绑定的函数原型如下:
#include<sys/types.h>
#include<sys.socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
4.3 监听本地端口listen()
服务器模式下同时有listen()
和accept()
两个函数,而客户端则不需要。函数listen()
用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序的,同一时间仅能处理一个客户端连接。当多个客户端的链接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端链接请求防盗等待队列中,这个队列的长度由listen()
函数来定义。
listen()
函数的原型如下,其中backlog
标示等待队列的长度。
#include<sys/socket.h>
int listen(int sockfd, int backlog);
4.4 接受一个网络请求accept()
当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的链接会在队列中等待,直到使用服务器处理接收的请求。
函数accept()
成功执行后,会返回一个新的套接口文件描述符来标示客户端的连接,客户端链接的信息可以通过这个新文件描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符标示正在监听的socket
,新产生的文件描述符表示客户端的连接,函数send()
和recv()
通过新的文件描述符进行数据收发。
accept()
函数的原型如下:
#include<sys/types.h>
#inclue<sys/