第一章 网络编程基础知识
网络由节点和连线构成。现实用应用中的网络由硬件设备(路由器、交换机、网线)+应用软件组成。
计算机网路技术发展的第一个里程碑以报文或分组交换技术的出现为标志。
数据交换的三种主要形式:
- 电路交换:在电路交换方式中,通过网络节点(交换设备)在工作站之间建立专用的通信通道,即在两个工作站之间建立实际的物理连接。一旦通信线路建立,这对端点就独占该条物理通道,直至通信线路被取消。
- 报文交换:报文是一个带有目的端信息和控制信息的数据包。报文交换采取的是“存储—转发” 方式,不需要在通信的两个节点之间建立专用的物理线路。
- 分组交换:它是报文交换的一种改进,也属于存储-转发交换方式,但它不是以报文为单位,而是以长度受到限制的报文分组为单位进行传输交换的。
计算机网络技术发展的第二个里程碑:开放式系统互联参考模型OSI/RM
第三个里程碑:Internet的迅速发展与推广。
OSI模型层次图:
OSI模型各层对应的实际对象举例如下:
应用层--- 计算机:应用程序,如FTP,SMTP,HTTP等
表示层--- 计算机:编码方式,图像编解码、URL字段编码等
会话层--- 计算机:建立会话,SESSION认证、断点续传等
传输层---计算机:进程和端口等
网络层--- 网络:路由器,防火墙、多层交换机等
数据链路层--- 网络:网卡,网桥,交换机等
物理层--- 网络:中继器、集线器、HUB等
TCP/IP模型与OSI模型对照:
TCP/IP模型中的数据传输:
TCP/IP协议的特点:
- 开放的协议标准
- 独立于特定的计算机硬件与操作系统
- 独立于特定的网络硬件。可以运行在局域网、广域网,更适用于互联网中
- 统一的网络地址分配方案,网络中的设备都有唯一的地址
- 标准化的高层协议,可以提供多种可靠的服务
C/S客户服务器模型的优缺点:
优点:
- C/S架构的界面和操作可以很丰富
- 安全性能可以个很容易保证,实现多层认证也不难
- 由于只有一层交互,因此响应速度较快
缺点:
- 用户群固定。程序需要安装才能使用,不适合面向一些不可知的用户
- 维护成本高,发生一次升级,所有的客户端程序都需要改变。
B/S架构的全称为Browser/Server,即浏览器/服务器结构。主要事务逻辑在服务器端实现,B/S架构的系统无须特别安装,只有Web浏览器即可。因此也被成为瘦客户端。
必须强调的是C/S和B/S并没有本质的区别:B/S是基于特定通信协议(HTTP)的C/S架构,也就是说B/S包含在C/S中,是特殊的C/S架构。
B/S优点和缺点
优点:
1)客户端无需安装,有Web浏览器即可。
2)B/S架构可以直接放在广域网上,通过一定的权限控制实现多客户访问的目的,交互性较强。
3)B/S架构无需升级多个客户端,升级服务器即可。
缺点:
1)在跨浏览器上,B/S架构不尽如人意。
2)表现要达到C/S程序的程度需要花费不少精力。
3)在速度和安全性上需要花费巨大的设计成本,这是B/S架构的最大问题。
4)客户端服务器端的交互是请求-响应模式,通常需要刷新页面,这并不是客户乐意看到的。
无连接和面向连接的服务器
UDP:无连接交互
没有可靠保证
依赖下层系统保证
程序中应该有相应的保障措施
TCP:面向连接的交互
提供传输可靠性
程序要求简单
选用UDP的情况:
下层系统可靠(例如在局域网环境)
应用不需要额外的可靠处理
广播或者组播
无状态和有状态服务器
服务器所维护的与客户交互的信息成为状态信息。不保存任何状态信息的服务器成为无状态服务器,反之成为有状态服务器。
状态服务器在服务器中保存少量信息,可减少客户端与服务器端交换报文的大小,保存了客户之前有过的请求,允许服务器快速的相应请求。
无状态服务器的动机是协议的不可靠性。通常情况下报文丢失、重复或交付失序,或者客户端程序崩溃都会使服务器的状态信息不正确,此时就可能产生不正确的响应。无状态服务器则不会因为这些原因出现问题。
有状态服务器具有以下特点:
- 保存客户请求的数据(状态)
- 服务端容易对客户状态进行管理
- 服务端并不要求每次客户请求都携带额外的状态数据
无状态服务器具有以下特点:
- 不保存客户请求的数据
- 客户在请求时需要携带额外的状态数据
- 无状态服务器更加健壮,重启服务器不会丢书状态信息,这使得维护和扩容更加简单
无状态服务器最著名的就是WEB服务器
互联网结构有三个基本原则,交互性,开放性,端到端
互联网上的网络应用需求、使用方法千差万别,但可以大体上分为三类:
按通信方式:“客户端/服务器” “游览器/WEB服务器”和“点对点”
第二章 客户服务器软件中的并发处理
1、并发、并行的概念及其区别。
并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。
并行:当系统有一个以上CPU时, 当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
区别:
并行是指两个或者多个事件在同一时刻发生;
并发是指两个或多个事件在同一时间间隔内发生。是指在一段时间内宏观上有多个程序在同时运行,在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。
如果在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。
2、进程、线程的概念及其联系和区别
进程的概念:进程是表示资源分配的基本单位。它是一个执行某一个特定程序的实体,它拥有独立的地址空间、执行堆栈、文件描述符等。
线程的概念:有时被称为轻量级进程,线程是进程中执行运算的最小
单位,亦即执行处理机调度的基本单位。
进程与线程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
并发处理机制使程序功能更强,但也会增加一些计算开销
OS的时间分片机制保证快速切换
线程切换的时候,会发生上下文切换
上下文:线程的执行环境
切换的开销
设计协议软件的时候,需要设法将上下文切换的次数减到最少
保证并发处理的好处比上下文切换的开销多
服务器的设计策略包括:非并发,单线程进程并发,多线程进程并发
3、阻塞、非阻塞、同步和异步的概念,阻塞I/O,非阻塞I/O,多路复用I/O,信号驱动I/O,异步 I/O 的原理及其示意图。
阻塞:
是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
阻塞调用和同步调用实际上是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
非阻塞:
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
对比阻塞与非阻塞:
阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。
非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的 CPU 核心利用率永远是 100%,I/O 以事件的方式通知。
在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让 CPU 资源不被阻塞中的线程浪费。
而在非阻塞模式下,线程不会被 I/O 阻塞,永远在利用 CPU。多线程带来的好处仅仅是在多核 CPU 的情况下利用更多的核。
同步:
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何工作;ATM存款
异步:
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求通过事件触发->服务器处理(这时浏览器仍然可以作其他事情)->处理完毕
总结:
阻塞和非阻塞是指当的进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪;
而同步和异步是访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后便可以继续处理其它任务,随后等待网络通信(I/O操作)完毕的通知,这可以使进程在数据读写时也不阻塞。
五种I/O模型:
- 阻塞I/O
- 非阻塞I/O
- I/O复用(select和poll)
- 信号驱动I/O
- 异步I/O
前四种是同步的,最后一种是异步
Linux fork实现原理
fork函数用来创建一个新的进程,fork将正在运行的程序分成2个几乎完全一样的进程,每个进程都启动一个从代码同一位置开始的线程。fork后,父子进程具有相同的数据空间、代码空间、堆栈和所有文件描述符,但相互之间互不影响。fork函数,在父进程中返回子进程的id,在子进程中返回0,出错返回-1,并发进程利用这个区别让新进程执行与原来不一样的代码。
执行新的代码:
int execl(const char *path, const char *arg, ...);
系统调用execl执行另一个程序。调用execl并不创建新进程,所以前后的进程ID并未改变,execl只是用另一个新程序替换了当前进程的正文、数据、堆栈;
path 是要执行的二进制文件或脚本的完整路径。
arg是要传给程序的完整参数列表,包括arg[0],一般是执行程序的名字。
最后一个参数可为NULL。
第三章 基本套接口编程
不精确指明的协议软件接口
TCP/IP和应用程序之间的接口应该是不精确指明的:
不规定接口的细节
只建议需要的功能集
允许系统设计者选择有关API的具体实现细节
优点:
提供了灵活性和容错能力,便于各种OS实现TCP/IP;接口可以是过程的,也可以是消息的
缺点:
不同OS中的接口细节不同,厂商增加与现有API不同的接口时,编程更困难,移植性差;程序员需要重新学习接口知识。
目前存在几种TCP/IP接口:
Berkeley UNIX中的TCP/IP API,成为套接字接口
Microsoft windows中TCP/IP API,windows socket
AT&T的UNIX系统V的TLI
什么是套接字?
1、套接字是一个主机本地网络应用程序所创建的,为操作系统所控制的接口。
2、应用程序通过这个接口,使用传输层提供的服务,跨网络发送或接收消息
3、Client/server模式的通信接口—套接字接口
套接字概要—主动套接字和被动套接字
创建方式相同,使用方式不同
等待传入连接的套接字—被动,如服务器套接字
发起连接的套接字—主动,如客户套接字
指明端点地址:创建时不指定,使用时指明,允许协议族自由的选择地址表示方式
套接字的普通C定义结构
struct_sockaddr{
u_char sa_len;//1B
u_short sa_fanily;//2B
char sa_data[14];//14B
}通用的的地址结构
TCP/IP的地址定义
struct sockaddr_in{
u_char sin_len;//1B
u_short sin_family;//2B
u_short sin_port;//2B
struct in_addr sin_addr;//4B
char sin_zero[8];//8B
}IP专用地址结构
结构体in_addr用来表示一个32位的IPv4地址。其字节序为网络字节序。
主调用的套接字API
int Socket(int domain, int type, int protocol)
功能:创建一个新的套接字,返回套接字描述符
参数说明:
domain:域类型,指明使用的协议栈,AF_INET:IPv4协议;AF_INET6:IPVC6协议;。。。。。。
type:指明需要的服务类型,AF_INET地址族如下
SOCK_DGRAM:数据报服务,UDP协议
SOCK_STREAM:流服务,TCP协议
SOCK_RAW:提供传输层以下的协议,例如发送和接收ICMP报文
protocol:一般为0(由系统根据服务类型选择默认的协议)
int bind(int sockfd,struct sockaddr * my_addr,int addrlen)
功能:为套接字指明一个本地端点地址
TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号
服务器使用它来指明熟知的端口号,然后等待连接
参数说明:
Sockfd:套接字描述符,指明创建连接的套接字
my_addr:本地地址,IP地址和端口号
addrlen :地址长度
为什么TCP服务端需要调用bind函数而客户端通常不需要呢?
客户端也可以bind,但是一般操作系统会为之分配不冲突的端口,自己bind可能会冲突。
int listen(int sockfd, int input_queue_size)
功能:
面向连接的套接字使用它将一个套接字置为被动模
式,并准备接收传入连接。用于服务器,指明某个
套接字连接是被动的
参数说明:
Sockfd:套接字描述符,指明创建连接的套接字
input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数
请将套接字设为被动模式,并允许最大请求数为20。
举例:listen(sockfd,20)
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
为每个新的连接请求创建了一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接收其他的连接请求。
新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。
参数说明:
Sockfd:套接字描述符,指明正在监听的套接字
addr:提出连接请求的主机地址
addrlen:地址长度
举例:new_sockfd = accept(sockfd, (struct sockaddr *)&address, sizeof(address));
int connect(int sockfd, struct sockaddr *server_addr, int sockaddrlen)
功能: 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。
参数说明:
Sockfd:套接字描述符,指明创建连接的套接字
Server_addr:指明远程端点:IP地址和端口号
sockaddr_len :地址长度
举例(P49): connect(s,remaddr,remaddrlen)
int sned(int sockfd, const void *data, int data_len, unsigned int falgs)
功能:在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1
send会将外发数据复制到OS内核中,也可以使用send发送面向连接的UDP报文。
参数说明:
sockfd:套接字描述符
data:指向要发送数据的指针
data_len:数据长度
flags:通常为0,设置为 MSG_DONTWAIT为非阻塞
记住如果send()函数的返回值小于len 的话,则你需要再次发送剩下的数据。802.3,MTU为1492B,如果包小于1K,那么send()一般都会一次发送光的。
int sendto(int sockfd, const void * data, int data_len, unsigned int flags, struct sockaddr *remaddr,sock_len remaddr_len)
功能:基于UDP发送数据报,返回实际发送的数据长度,出错时返回-1
参数说明:
sockfd:套接字描述符
data:指向要发送数据的指针
data_len:数据长度
flags:通常为0,设置为 MSG_DONTWAIT为非阻塞
remaddr:远端地址:IP地址和端口号
remaddr_len :地址长度
int recv(int sockfd, void *buf, int buf_len, unsigned int flags);
功能:从TCP接收数据,返回实际接受的数据长度,出错时返回-1.
服务器使用其接受客户请求,客户使用它接收服务器的应答。如果没有数据,将阻塞。
如果TCP收到的数据大于(/小于)缓存的大小,只抽出能够填满缓存的足够数据(/抽出所有数据并返回它实际接收的字节数)。
也可以使用recv接收面向连接的UDP的报文,若缓存不能装下整个报文,填满缓存后剩下的数据将被丢弃。
参数说明:
Sockfd:套接字描述符
Buf:指向内存块的指针
Buf_len:内存块大小,以字节为单位
flags:一般为0(MSG_WAITALL接收到指定长度数据时才返回),设置为 MSG_DONTWAIT为非阻塞
int recvfrom(int sockfd, void *buf, int buf_len,unsigned int flags,struct sockaddr *from,sock_len *fromlen);
功能:从UDP接收数据,返回实际接收的字节数,失败时返回-1
参数说明:
Sockfd:套接字描述符
buf:指向内存块的指针
buf_len:内存块大小,以字节为单位
flags:一般为0
from:远端的地址,IP地址和端口号
fromlen:远端地址长度
举例:recvfrom(sockfd,buf,8192,0,(struct sockaddr *)&address, &sizeof(address));
close(int sockfd);
功能:
撤销套接字.
如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则关闭连接并撤销套接字。
参数说明:
Sockfd:套接字描述符
举例:close(socket_descriptor)
字节序转换
大于一个字节的变量类型的表示方法有两种:
小端字节序(Little Endian,LE):在表示变量的内存地址的起始地址存放低字节,高字节顺序存放;
大端字节序(Big Endian,BE):在表示变量的内存地址的起始地址存放高字节,低字节顺序存放
字节序转换函数
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); /*主机字节序到网络字节序的长整型转换*/
uint16_t htons(uint16_t hostshort); /*主机字节序到网络字节序的短整型转换*/
uint32_t ntohl(uint32_t netlong); /*网络字节序到主机字节序的长整型转换*/
uint16_t ntohs(uint16_t netshort); /*网络字节序到主机字节序的短整型转换*/
套接字描述符和通用文件描述符在形式上没有区别,那么如何判断一个文件描述符是否是套接字描述符呢?进行套接字描述符判定的函数issockettype(),返回值为1,是套接字描述符,0为文件描述符。
第四章 TCP网络编程基础
向内核传入数据的函数有send()、bind()等,从内核得到数据的函数有accept()、recv()等。
TCP网络编程有两种模式,一种是服务器模式,另一种是客户端模式。服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。
TCP客户服务器编程服务端流程主要分为:
1)套接字初始化(socket())
2)套接字与端口的绑定(bind())
3)设置服务器的侦听连接连接(listen())
4)接受客户端连接请求(accept())
5)接收和发送数据(read()、write())并进行数据处理
6)处理完毕的套接字关闭(close())
TCP客户服务器编程客户端流程主要分为:
1)套接字初始化(socket())
2)连接服务器(connect())
3)读写网络数据(read()、write())并进行数据处理
4)最后套接字关闭(close())过程。
客户端与服务器在连接、读写数据、关闭过程中有交互过程。
当服务器端在接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式和过程与普通文件的操作方式一致,内核会根据文件描述符的值来查找所对应的属性,当为套接字的时候,会调用相对应的内核函数。
ssize_t write(int fd, const void*buf,size_t nbytes);
int send(int sockfd,void *buf,int len,int flags)
返回实际所写入或发出的字节数,小于0表示出现了错误
使用read()函数可以从套接字描述符中读取数据。在读取数据之前,必须建立套接字并连接。
ssize_t read(int fd,void *buf,size_t nbyte)
int recv(int sockfd,void *buf,int len,int flags)
返回实际所读的字节数,如果返回的值是0,表示已经读到文件的结束了,小于0表示出现了错误
第五章 UDP网络编程基础
UDP协议的程序设计框架
,客户端和服务器之间的差别在于服务器必须使用bind()函数来绑定侦听的本地UDP端口,而客户端则可以不进行绑定,直接发送到服务器地址的某个端口地址。
UDP协议服务器和客户端之间的交互
UDP协议中服务器和客户端的交互存在于数据的收发过程中。进行网络数据收发的时候,服务器和客户端的数据是对应的:客户端发送数据的动作,对服务器来说是接收数据的动作;客户端接收数据的动作,对服务器来说是发送数据的动作。
UDP服务器编程框架
服务器流程主要分为下述6个部分,即建立套接字、设置套接字地址参数、进行端口绑定、接收数据、发送数据、关闭套接字等。
(1)建立套接字文件描述符,使用函数socket(),生成套接字文件描述符。
(2)设置服务器地址和侦听端口,初始化要绑定的网络地址结构。
(3)绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定。
(4)接收客户端的数据,使用recvfrom()函数接收客户端的网络数据。
(5)向客户端发送数据,使用sendto()函数向服务器主机发送数据。
(6)关闭套接字,使用close()函数释放资源。
UDP客户端编程框架
UDP协议的客户端流程分为套接字建立、设置目的地址和端口、向服务器发送数据、从服务器接收数据、关闭套接字等5个部分。流程如下:
(1)建立套接字文件描述符,socket();
(2)设置服务器地址和端口,struct sockaddr_in;
(3)向服务器发送数据,sendto();
(4)接收服务器的数据,recvfrom();
(5)关闭套接字,close()。
UDP报文的正常发送过程
而在Internet上,由于要经过多个路由器,正常情况下一个数据报文从主机C经过路由器A、路由器B、路由器C到达主机S,数据报文的路径如图所示。主机C使用函数sendto()发送数据,主机S使用recvfrom()函数接收数据,主机S在没有数据到来的时候,会一直等待。
UDP报文的丢失
路由器要对转发的数据进行存储、处理、合法性判定、转发等操作,容易出现错误,所以很可能在路由器转发的过程中出现数据丢失的现象,如图所示。当UDP的数据报文丢失的时候,函数recvfrom()会一直阻塞,直到数据到来。
UDP报文丢失的对策
主机C发送的数据经过路由器,到达主机S后,主机S要发送一个接收到此数据报文的响应,主机C要对主机S的响应进行记录,直到之前发送的数据报文1已经被主机S接收到。如果数据报文在经过路由器的时候,被路由器丢弃,则主机C和主机S会对超时的数据进行重发。
UDP报文乱序的对策
对于乱序的解决方法可以采用发送端在数据段中加入数据报序号的方法,这样接收端对接收到数据的头端进行简单地处理就可以重新获得原始顺序的数据。
UDP缺乏流量控制
UDP协议没有TCP协议所具有的滑动窗口概念,接收数据的时候直接将数据放到缓冲区中。如果用户没有及时地从缓冲区中将数据复制出来,后面到来的数据会接着向缓冲区中放入。当缓冲区满的时候,后面到来的数据会覆盖之前的数据造成数据的丢失。
解决UDP接收缓冲区溢出的现象需要根据实际情况确定,一般可以用增大接收数据缓冲区或接收方接收单独处理的方法来解决局部的UDP数据接收缓冲区溢出问题。
第六章 客户软件设计中的算法
标识服务器位置的几种方式
客户服务器网络编程中需要找到服务器的IP和端口:
1)在编译程序时,将服务器的域名或者IP地址说明为常量
执行快,但是服务器移动后不便
2)要求用户在启动程序时指定服务器
使用机器名,不必重新编译客户程序
3)从稳定的存储设备中获得关于服务器的信息
如果文件不存在,客户软件就不能执行
4)使用某个单独的协议来找到服务器(如广播或组播)
只能在本地小环境下应用
TCP客户算法-面向连连接的客户
1)分配套接字
2)找到期望与之通信的服务器IP地址和协议端口号
3)指明此连接需要在本地机器中的、任意的、未使用的协议端口,并允许TCP选择一个这样的端口
4)将这个套接字连接到服务器
5)使用应用级协议与服务器通信
6)关闭连接
分配套接字
<sys/types.h> //primitive system data types(包含 很多类型重定义,如pid_t、int8_t等)
<sys/socket.h> //与套接字相关的函数声明和结构体定义, 如socket()、bind()、connect()及struct sockaddr的定义
<sys/ioctl.h> //I/O控制操作相关的函数声明,如ioctl()
<stdlib.h> //某些结构体定义和宏定义,如 EXIT_FAILURE、EXIT_SUCCESS等
<netdb.h> //某些结构体定义、宏定义和函数声明, 如struct hostent、struct servent、 gethostbyname()、gethostbyaddr()、herror()等
<arpa/inet.h> //某些函数声明,如inet_ntop()、 inet_ntoa()等
<netinet/in.h> //某些结构体声明、宏定义,如struct sockaddr_in、PROTO_ICMP、INADDR_ANY等
选择本地协议端口号
服务器运行于熟知的端口上,客户端并非某个预分配的端口上。
客户使用端口的规则:
该端口不与该机器其他进程使用端口冲突
该端口没有被分配给某个熟知服务
客户允许TCP自动选择本地端口
connect调用的一个效果就是所选择的本地端口能满足上述准则。
将TCP套接字连接到服务器
connect函数:允许TCP套接字发起连接
执行下层的三次握手
超时或者建立连接后返回
三个参数:
retcode = connect(s, remaddr, remaddrlen);
s: 套接字的描述符
remaddr:一个sockaddr_in类型结构的地址
remaddrlen:第二个参数的长度
connect的四项任务
对指明的套接字进行检测:有效,还没有连接
将第二个参数给出的端点地址填入套接字中
为此套接字选择一个本地端点地址
发起一个TCP连接,并返回一个值
UDP客户端基本算法
- 分配套接字
- 找到期望与之通信的服务器IP地址和协议端口号
- 指明这种通信需要本地机器中的、任意的、未使用的协议端口,并允许UDP选择一个这样的端口
- 指明报文所要发往的服务器
- 使用应用级协议与服务器通信
- 关闭连接
使用UDP与客户端通信
对于连接的UDP套接字
使用send发送报文
使用recv接收报文
每次send发送一个完整的报文
每次recv接受一个完整的报文,足够大缓存
不需要重复使用recv获得单个报文
对于非连接的UDP套接字
sendto: 发送报文,含有地址信息
recvfrom:接收一个含有源地址的数据报
DAYTIME获取服务器的时间。
Time服务允许一台机器从另外一台机器获得当前日期和时间。
由于存在不同的时区,所有的时间日期信息必须用国际标准时间:UCT或UT
服务器应答前将本地时间转化为标准时间
客户收到应答时,将国际标准时间转化为本地时间。
TIME协议规定由32bit的整数来表示
从1900年1月1日午夜为起点的秒数
用于一台计算机使用另外一系统时钟来设置日期时间
TIME服务使用端口37
可以使用TCP协议
使用TCP的TIME服务器利用连接的出现激活输出,类似DAYTIME服务。
使用TCP的客户不用发送任何数据
也可以使用UDP访问TIME服务
客户发出包含单个数据报的请求
服务器从传入的数据报中取出地址和端口号
服务器将当前时间编码为一个整数,使用上述地址和端口号发回给客户
ECHO服务器返回从客户收到的所有数据
用户网络管理员测试可达性,调试协议软件,识别选路问题等
TCP ECHO服务:接收连接请求,从连接中读取数据,在该连接上将数据写回。直到客户终止传送。
UDP ECHO服务:接收整个数据报,根据数据报指明的端口号和地址,返回整个数据报
Socket 是unsigned int的类型,2个字节,所以其范围
0--65535
但0-1024,被系统分配,ftp、telent、http、daytime、time等等,都是系统已经分配固定端口,因此可以通过getservbyname来获取其端口号的。
用户自定义的端口号建议:1025--65535
第七章 服务器软件设计算法
概念性的服务器算法
各个服务器遵循一种简单的算法:
创建套接字
绑定到一个熟知端口
期望在这个端口上接收请求
进入无限循环,接受客户请求并应答
并发服务器和循环服务器
循环服务器:一个时刻只处理一个请求,FIFO
并发服务器:一个时刻可以处理多请求
多数只提供表面并发:执行多个线程,每个线程处理一个请求
使用单线程的可能性:每次服务计算量小,主要是I/O复用, 便于同时使用多个通信信道
并发服务器指服务器是否并发处理多个请求,而不是指下层是否使用了多个并发线程
循环服务器容易构建,在服务端处理复杂的情况下性能差;并发服务器难以构建和设计,在服务端处理复杂的情况下但是性能好
TCP的语义
点到点通信
建立可靠连接
可靠交付
具有流控的传输
双工传输
流模式
UDP的语义
多对多通信
不可靠服务
缺乏流控制
报文模式
面向连接的服务器的优缺点
面向连接服务的优点:
易于编程
自动处理分组丢失,分组失序
自动验证数据差错,处理连接状态
面向连接服务的缺点:
对每个连接都有一个单独的套接字,耗费更多的资源
在空闲的连接上不发送任何分组
始终运行的服务器会因为客户的崩溃,导致无用套接字的过多而耗尽资源,终止运行
无连接服务器优缺点
优点:没有资源耗尽问题
缺陷:需要自己完成可靠通信问题
必要时,需要一种自适应重传的复杂技术,需要程序员具有相当的专业知识
对于不可靠通信的场合,尽量使用tcp
是否需要组播或者广播是考虑选择何种传输方式的一个因素
支持组播或者广播的服务器必须是无连接的,今后会不断增加这样的应用。
循环面向连接的服务器算法
通过TCP的面向连接的循环服务器算法
1、创建套接字并将其绑定到它所提供服务的熟知端口上;
2、将该端口设置为被动模式,使其准备为服务器所用;
3、从该套接字上接收下一个连接请求,获得该连接的新的套接字;
4、在新套接字上重复地读取来自客户的数据,构造响应,按照应用协议向客户发回响应;
5、当某个特定客户完成交互时,关闭连接,并返回步骤3以接受新的连接。
无连接循环服务器的算法
循环服务器的设计,编程,排错,修改很容易。往往使用无连接的协议。
循环服务器对于小的处理时间的服务工作很好。
无连接服务器算法如下:
1、创建套接字并将其绑定到所提供服务的熟知端口上;
2、重复读取来自客户的请求,构造响应,按照应用协议向客户发回响应。
并发服务器
主线程和从线程
尽管可以使用一个单线程实现并发服务器,但是大多数使用多线程:
主线程先执行在熟知的端口上打开一个套接字,等待一个请求,并为每个请求创建一个从线程(可能在一个新进程中)
主线程不与客户直接通信,每个从线程处理一个客户的通信。
从线程构成响应并发送给客户后,这个从线程便退出
并发无连接服务器算法
最简单的算法:
主1、创建套接字并将其绑定到所提供服务的熟知地址上。让该套接字保持为未连接的
主2、反复调用recvfrom接收来自客户的下一个请求,创建一个新的从线程来处理响应
从1、从来自主线程的特定请求以及到该套接字的访问开始
从2、根据应用协议构造应答,并用sendto将该应答发回给客户
从3、退出(即:从线程处理完一个请求后就终止)
由于创建进程或者线程是昂贵的,因此只有很少的无连接服务器采用并发实现
并发面向连接的服务器算法:
面向连接的服务器在多个连接之间实现并发
主1、创建套接字并将其绑定到所提供服务的熟知地址上。让该套接字保持为无连接的
主2、将该端口设置为被动模式
主3、反复调用accept以便接收来自客户的下一个连接请求,并创建新的从线程或者进程来处理响应
从1、由主线程传递来的连接请求开始
从2、用该连接与客户进行交互;读取请求并发回响应
从3、关闭连接并退出
单线程面向并发、面向连接的服务器算法
1、创建套接字并将其绑定到这个服务的熟知端口上,将该套接字加到一个表中,该表中的项是可以进行I/O的描述符。
2、使用select在已经有的套接字上等待I/O
3、如果最初的套接字准备就绪,使用accept获得下一个连接,并将这个新的套接字加入到表中,该表中的项是可以进行I/O的描述符。
4、如果最初的套接字以外的套接字就绪,就使用recv或read获得下一个请求,构造响应,用send或者write将响应发回给客户
5、继续按照以上的步骤2进行处理
各个服务器的适用场合
循环的和并发的:
如果循环方案产生的响应时间对应用来说足够,就可以使用循环;否则需要并发
真正的和表面上的并发性:
创建线程或切换环境的开销大,服务器需要在多个连接之间共享或者交换数据,用单线程;
使用线程开销不大,服务器需要在多个连接之间共享或者交换数据,用多线程;
每个进程可以孤立运行或者要得到最大并发性,使用多进程
面向连接的和无连接的:
当应用协议处理了可靠性问题,或者应用在局域网环境内时,使用无连接的传输。否则使用有连接传输。
服务器类型小结
循环无连接服务器
对每个请求的处理少,通常为无状态的
循环的面向连接服务器
要求可靠传输的,对每个请求处理少的服务
并发的,无连接的服务器
不常见,为每个请求创建一个新线程或进程
并发的面向连接的服务器
最一般的。可靠传输,并发处理多个请求
多进程可以是多个独立的程序
多线程(进程)或者单线程方式