前言
几种典型的应用编程接口
- Berkeley UNIX操作系统定义了一种API,称为套接字接口(socket interface),简称套接字Socket
- 微软公司在其操作系统中采用了套接字API,形成了一个稍有不同的API,并称之为Windows Socket Interface,WinSock
- AT&T为其UNIX系统V定义了一种API,简写为TLI(Transport Layer Interface)
Socket API
- 最初设计:面向BSD Unix-Berkeley,面向TCP/IP协议栈接口
- 目前:事实上的工业标准,绝大多数操作系统支持
- Internet网络应用最典型的API接口
- 通信模型:客户/服务器(C/S)
- 应用进程间通信的抽象机制
- 标识通信端点(对外):IP地址+端口号
- 操作系统/进程如何管理套接字(对内)?套接字描述符(Socket descriptor):进程创建套接字时的返回值
Socket 抽象
- 类似文件的抽象
- 当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息
- 返回套接字描述符
struct sockaddr_in {
u_char sin_len; // 地址长度
u_char sin_family; // 地址族
u_short sin_port; // 端口号
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 保留位,置0
}
Socket API 函数(WinSock为例)
WSAStartup 函数
使用Socket的应用程序在使用Socket之前必须首先调用WSAStartup函数
int WSAStartup(WORD wVersionRequested,LPWSADATA IpWSAData);
两个参数:
- 第一个参数指明程序请求使用的WinSock版本,其中高位字节指明副版本、低位字节指明主版本。
- 十六进制整数,例如0x102表示2.1版
- 第二个参数返回实际的WinSock的版本信息
- 指向WSADATA结构的指针
例:使用2.1版本的WinSock的程序代码段
wVersionRequested = MAKEWORD(2, 1);
err = WSAStartup(wVersionRequested, &wsaDara);
WSACleanup 函数
应用程序在完成对请求的Socket库的使用,最后要调用WSACleanup函数,解除与Socket库的绑定,释放Socket库所占用的系统资源。
int WSACleanup(void)
Socket 函数
创建套接字,操作系统返回套接字描述符(sd)
sd = socket(protofamily, type, proto);
- 第一个参数(协议族):对于TCP/IP协议族,protofamily = PF_INET(TCP/IP)
- 第二个参数(套接字类型)
- 第三个参数(协议号):0为默认
CloseSocket 函数
关闭一个描述符为sd的套接字,如果多个进程共享一个套接字,调用closesocket将套接字引用计数减1,减至0才关闭。
int closesocket(SOCKET sd);
一个进程中的多线程对一个套接字的使用无计数:如果进程中的一个现场调用closesocket将一个套接字关闭,该进程中的其他线程也将不能访问该套接字。
Bind 函数
绑定套接字的本地端点地址:IP地址+端口号
int bind(sd, localaddr, addrlen);
- 套接字描述符(sd)
- 端点地址
- 客户程序一般不必主动调用bind函数,操作系统将自动调用。
Listen 函数
置服务器端的流套接字处于监听状态。
- 仅服务器端调用
- 仅用于面向连接的流套接字
- 设置连接请求队列大小(queuesize)
int listen(sd, queuesize);
connect 函数
客户端程序调用connect函数来使客户套接字(sd)与特定计算机的特定端口(saddr)的套接字(服务)进行连接。其仅用于客户端。
int connect(sd, saddr, saddrlen);
accept 函数
服务端调用accept函数从处于监听状态的流套接字sd的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户端套接字创建连接通道。
- 仅用于TCP套接字
- 仅用于服务器
newsock = accept(sd, caddr, caddrlen);
利用新创建的套接字(newsock)与客户通信,保证服务器可以并发。
send、sendto 函数
send函数TCP套接字(客户与服务器)或调用了connect函数的UDP客户端套接字。
sendto函数用于UDP服务器端套接字与未调用connect函数的UDP客户端套接字。
send(sd, *buf, len, flags);
sendto(sd, *buf, len, flags, destaddr, addrlen);
recv、recvfrom 函数
recv函数从TCP连接的另一端接收数据,或者从调用了connect函数的UDP客户端套接字接收服务器发来的数据。
recvfrom函数从UDP都武器端套接字与未调用connect函数的UDP客户端套接字接收对端数据。
recv(sd, *buffer, len, flags);
recvfrom(sd, *buf, len, flags, sendaddr, saddrlen);
setsockopt、getsockopt 函数
setsockopt() 函数用来设备套接字sd的选项参数
getsocket()函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入optval。
int setsocketopt(int sd, int level, int optname, *optval, int optlen);
int getsocketopt(int sd, int level, int optname, *optval, socklen_t optlen);
数据解析
网络字节顺序
TCP/IP 定义了标准的用于协议头中的二进制整数表示:网络字节顺序(network byte order)。
某些Socket API函数的参数需要存储为网络字节顺序(如IP地址、端口号等)。这时候需要实现本地字节顺序与网络字节顺序转换的函数:
- htons:本地字节顺序 -> 网络字节顺序(16bits)
- ntohs:网络字节顺序 -> 本地字节顺序(16bits)
- htonl:本地字节顺序 -> 网络字节顺序(32bits)
- ntohl:网络字节顺序 -> 本地字节顺序(32bits)
解析服务器IP地址、端口号
客户端可能使用域名(如:study.163.com)或 IP地址(如:123.58.180.121)表示服务器。IP协议需要使用32位二进制IP地址。
需要将域名或IP地址转换为32位IP地址。
- 函数
inet_addr()
实现点分十进制IP地址到32位IP地址转换 - 函数
gethostbyname()
实现域名到32位IP地址转换:返回一个指向结构hostent的指针
客户端还可能使用服务名(如HTTP)表示服务器端口。
需要将服务名转换为熟知的端口号。
- 函数
getservbyname()
:返回一个指向结构servent的指针
解析协议号
客户端可能使用协议名(如:TCP)指定协议,需要将协议名转换为协议号(如:6)。
- 函数
getprotobyname()
实现协议名到协议号的转换:返回一个指向结构protoent的指针
TCP/UDP客户端软件流程
TCP:
- 确定服务器IP地址与端口号
- 创建套接字
- 系统自动分配本地端点地址(IP+端口号)
- 连接服务器(套接字)
- 遵循应用层协议进行通信
- 关闭/释放连接
UDP:
- 确定服务器IP地址与端口号
- 创建套接字
- 系统自动分配本地端点地址(IP+端口号)
- 指定服务器端点地址,构造UDP数据报
- 遵循应用层协议进行通信
- 关闭/释放套接字
服务器软件设计
四种类类型基本服务器
- 循环无连接服务器(iterative connectionless)
- 循环面向连接服务器(iterative connection-oriented)
- 并发无连接服务器(concurrent connectionless)
- 并发面向连接服务器(concurrent connection-oriented)
循环无连接服务器(iterative connectionless): 一次只处理一个客户的服务请求,直到这个服务请求处理完毕。
循环面向连接服务器(iterative connection-oriented)
并发无连接服务器(concurrent connectionless)
并发面向连接服务器(concurrent connection-oriented)