文章目录
2 应用层
2.7 Socket编程
2.7.1 应用编程接口(API)
- 应用层(Application):Web/RPC/中间件编程
- Socket编程
- NetBIOS编程——Windows
- 传输层(Transport)
- 网络层(Network)
- 基于NDIS网络编程——Windows
- 基于LibPcap/WinPcap、Libnet、Libnids、Libicmp编程
- 数据链路层(Data link)
- 直接网卡编程,硬件相关的
- 基于Packet Driver编程,屏蔽网卡细节,适用于所有网卡
- 物理层(Physical)
API:为了使应用层的应用进程可以和相邻层(传输层)传递数据,需要一个接口,即API。所以API就是应用进程的控制权和操作系统的控制权进行转换的一个系统调用接口
典型的应用编程接口:
socket interface(socket、套接字)——Berkeley UNIX
Windows Socket Interface(WINSOCK)——微软
Transport Layer Interface(TLI)——AT&T UNIX 系统 V
2.7.2 Socket API概述
标识通信端点(对外):IP地址+端口号(16位整数)
操作系统/进程管理套接字(对内):套接字描述符(socket descriptor)——小整数
Socket抽象
类似于文件的抽象:当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息,操作系统返回套接字描述符
操作系统维护一个套接字描述符表,存储的是指向套接字数据结构的指针
地址结构:
sockaddr_instruct aockaddr_in { u_char sin_len; /*地址长度*/ u_char sin_family; /*地址族*/ u_short sin_port; /*端口号*/ struct in_addr sin_addr; /*IP地址*/ char sin_zero[8]; /*未用(置0)*/ }地址族:为了适应不同协议的变化,引入了地址族,其在TCP/IP下使用的值为AF_INET
2.7.3 Socket API函数
所有的API都是在WINSOCK中的,与UNIX下的socket大同小异
2.7.3.1 WSAStartup
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
- 使用Socket的应用程序在使用Socket之前必须首先调用此函数加载DLL
- 第一个参数指明程序请求使用的WinSock版本:高位字节指明副版本、低位字节指明主版本
- 第二个参数是返回实际的WinSock的版本信息:指向WSADATA结构的指针
/* 例 */
wVersionRequested = MAKEWORD(2, 1);
err = WSAStartup(wVersionRequested, &wsaData);
2.7.3.2 WSACleanup
int WSACleanup (void);
应用程序在完成对请求的Socket库的使用,最后要调用WSACleanup函数解除与Socket库的绑定,释放Socket库所占用的系统资源
2.7.3.3 socket
sd = socket(protofamily,type,proto);
用于创建套接字
操作系统返回套接字描述符(sd)
第一个参数指明协议族:在TCP/IP下为PF_INET
第二个参数指明套接字类型:在TCP/IP下可以为SOCK_STREAM、SOCK_DGRAM或SOCK_RAW
第三个参数指明协议号:默认为0,当某类套接字只面向一类协议时直接使用0即可,而面向多个协议时就需要指定协议号了
Socket面向TCP/IP的服务类型
应用层 ------------- 应用进程
\qquad\qquad\qquad / \quad | \quad \
SOCK_STREAM \quad\; | \quad SOCK_DGRAM
\qquad \qquad / \;\; SOCK_RAW \quad \
传输层 — TCP \qquad\; | \qquad\quad UDP
网络层 ---------- IP/ICMP/IGMP
2.7.3.4 Closesocket
int closesocket(SOCKET sd)
unix下该函数名是
close,也就是关闭文件的函数该函数是关闭描述符为sd的套接字
- 但如果多个进程共享一个套接字,调用该函数是将套接字引用计数减1,减至0才关闭,它清除掉的只是该进程中对它的引用
- 而一个进程中的多线程对一个套接字的使用是无计数的,也就是说,在一个线程中关闭了一个套接字意味着其他线程也不能再访问该套接字了
返回0表示成功,返回SOCKET_ERROR表示失败
2.7.3.5 bind
int bind(sd, localadr, addrlen);
绑定套接字的本地端点地址:IP地址+端口号
- 由于在创建SOCKET的时候可能没有对应的地址信息,所以要进行地址信息的绑定
客户程序一般不需要调用bind函数,操作系统自动设置
服务端需要绑定端口号,而不能绑定特定的IP,一旦绑定了特定IP意味着其他IP就不能访问该服务器了,所以为了解决该问题,服务端绑定的是一个地址通配符INADDR_ANY
2.7.3.6 listen
int listen(sd, queuesize);
- C/S架构下将服务器端的流套接字置为监听状态,所以此函数仅被服务器调用,仅用于面向连接(TCP)的流套接字
- 第二个参数设置连接请求队列的大小,服务器从队列中提取
- 返回0表示成功,返回SOCKET_ERROR表示失败
2.7.3.7 connect
connect(sd,saddr,saddrlen);
使客户套接字(sd)与特定计算机的特定端口(saddr)的套接字(服务)进行连接
仅用于客户端,可用于TCP客户端,也可用于UDP客户端
- TCP客户端:建立TCP连接,客户端调用此函数对服务器发起连接请求
- UDP客户端:指定服务器端点地址,UDP是无连接的,所以即使调用
commect成功,也有可能无法与服务器通信
2.7.3.8 accept
newsock = accept(sd,caddr,caddrlen);
服务程序调用
accept从处于监听状态的流套接字sd的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道- 如果直接使用服务器的主套接字则服务器在同一时刻只能与一个客户端连接,所以使用创建新套接字来达到并行连接的目的(并发的TCP服务器)
仅用于服务器,仅用于TCP套接字
2.7.3.9 send, sendto
send(sd,*buf,len,flags);
sendto(sd,*buf,len,flags,destaddr,addrlen);
send是没有指定服务器的地址的,也就是说连接已经建立了,故可用于:- TCP套接字(客户端与服务器端均可)
- 调用了
connect的UDP客户端的套接字(连接模式的UDP套接字)
sendto函数用于UDP服务器端套接字与未调用connect函数的UDP客户端套接字
2.7.3.10 recv, recvfrom
recv(sd,*buffer,len,flags);
recvfrom(sd,*buf,len,flags,senderaddr,saddrlen);
recv函数从TCP连接的另一端接收数据,或者从调用了connect函数的UDP客户端套接字接收服务器发来的数据recvfrom函数用于从UDP服务器端套接字与未调用connect函数的UDP客户端套接字接收对端数据
2.7.3.11 setsockopt, getsockopt
int setsockopt(int sd, int level, int optname, *optval, int optlen);
int getsockopt(int sd, int level, int optname, *optval, socklen_t *optlen);
setsockopt函数用来设置套接字sd的选项参数getsockopt函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入optval
2.7.4 网络字节顺序
由于五层网络模型中不存在表示层,所以无法进行不同机器间的表示转化,于是TCP/IP定义了标准的用于协议头中的二进制整数表示:网络字节顺序(network byte order)
某些Socket API函数的参数需要存储为网络字节顺序(如IP地址、端口号等),因此实现本地字节顺序与网络字节顺序间转换的函数有:
- htons: 本地字节顺序→网络字节顺序(16bits)
- ntohs: 网络字节顺序→本地字节顺序(16bits)
- htonl: 本地字节顺序→网络字节顺序(32bits)
- ntohl: 网络字节顺序→本地字节顺序(32bits)
2.7.5 客户端软件设计
2.7.5.1 解析服务器IP地址
由于客户端可能使u用域名或IP地址来标识服务器,而IP协议需要32位的二进制IP地址,所以需要将域名或IP地址转换为32位的IP地址。
- 函数
inet_addr()实现点分十进制IP地址到32位IP地址转换 - 函数
gethostbyname()实现域名到32位IP地址转换,返回一个指向结构hostent的指针
以上两个函数得到的已经是网络字节顺序,可以直接使用。
2.7.5.2 解析服务器端口号
客户端还可能使用服务名(如HTTP)标识服务器端口,因此需要将服务名转换为熟知端口号。
- 函数
getservbyname(),返回一个指向结构servent的指针
2.7.5.3 解析协议号
客户端可能使用协议名(如:TCP)指定协议,因此需要将协议名转换为协议号(如:6)
- 函数
getprotobyname()实现协议名到协议号的转换,返回一个指向结构protoent的指针
2.7.5.4 TCP/UDP客户端软件流程
TCP:
确定服务器IP地址与端口号
创建套接字
分配本地端点地址(IP地址+端口号)——不需要设计软件时手动来做,系统自动完成
连接服务器(套接字)——
connect()遵循应用层协议进行通信——根据协议确定客户端和服务器哪方先发信息
关闭/释放连接
UDP:
- 确定服务器IP地址与端口号——并非只是每次的第一步做,之后可能每次都需要做
- 创建套接字
- 分配本地端点地址(IP地址+端口号)——自动完成
- 指定服务器端点地址,构造UDP数据报——
connect(),构造的UDP数据报可以发给不同的服务器,此时需要重复做第一步 - 遵循应用层协议进行通信——一定是UDP客户端先给服务器发信息
- 关闭/释放套接字
2.7.6 服务器软件设计
循环无连接(Iterative connectionless)服务器,基本流程:
- 创建套接字
- 绑定端点地址(INADDR_ANY+端口号)
- 反复接收来自客户端的请求
- 遵循应用层协议,构造响应报文,发送给客户
数据发送:
- 服务器端不能使用
connect()函数 - 无连接服务器使用
sendto()函数发送数据报
获取客户端点地址:
- 调用
recvfrom()函数接收数据时,自动提取
循环面向连接(Iterative connection-oriented)服务器,基本流程:
- 创建(主)套接字,并绑定熟知端口号;
- 设置(主)套接字为被动监听模式,准备用于服务器;
- 调用
accept()函数接收下一个连接请求(通过主套接字),创建新套接字用于与该客户建立连接; - 遵循应用层协议,反复接收客户请求,构造并发送响应(通过新套接字);
- 完成为特定客户服务后,关闭与该客户之间的连接,返回步骤3.
并发无连接(Concurrent connectionless)服务器,基本流程:
主线程1: 创建套接字,并绑定熟知端口号;
主线程2: 反复调用
recvfrom()函数,接收下一个客户请求,并创建新线程处理该客户响应;子线程1: 接收一个特定请求;
子线程2: 依据应用层协议构造响应报文,并调用
sendto()发送;子线程3: 退出(一个子线程处理一个请求后即终止)。
并发面向连接(Concurrent connection-oriented)服务器,基本流程:
主线程1: 创建(主)套接字,并绑定熟知端口号;
主线程2: 设置(主)套接字为被动监听模式,准备用于服务器;
主线程3: 反复调用
accept()函数接收下一个连接请求(通过主套接字),并创建一个新的子线程处理该客户响应;子线程1: 接收一个客户的服务请求(通过新创建的套接字);
子线程2: 遵循应用层协议与特定客户进行交互;
子线程3: 关闭/释放连接并退出(线程终止)。
客户端与服务器的实现范例参见PPT