Socket 编程
Socket 编程-应用编程接口(API)
网络程序设计接口
应用编程接口 API(Application Programming Interface)
应用编程接口 API:就是应用进程的控制权和操作系统的控制权进行转换的一个系统调用接口.
几种典型的应用编程接口:
- Berkeley UNIX 操作系统定义了一种 API,称为套接字接口(socket interface),简称套接字(socket)。
- 微软公司在其操作系统中采用了套接字接口 API,形成了一个稍有不同的 API,并称之为 Windows Socket Interface,WINSOCK。
- AT&T 为其 UNIX 系统 V 定义了一种 API,简写为 TLI(Transport Layer Interface)
Socket 编程-Socket API 概述
- 最初设计:面向 BSD UNIX-Berkley,面向 TCP/IP 协议栈接口
- 目前:事实上的工业标准,绝大多数操作系统都支持
- Internet 网络应用最典型的 API 接口
- 通信模型:客户/服务器(C/S)
- 应用进程间通信的抽象机制
标识通信端点(对外):IP 地址+端口号
操作系统/进程如何管理套接字(对内)?答:套接字描述符(socket descriptor),小整数
Socket 抽象
类似于文件的抽象,当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息,返回套接字描述符
地址结构
已定义结构 sockaddr_in:
structsockaddr_in
{
u_charsin_len; /*地址长度*/
u_charsin_family; /*地址族(TCP/IP:AF_INET)*/
u_shortsin_port; /*端口号*/
structin_addrsin_addr;/*IP地址*/
char sin_zero[8]; /*未用(置0)*/
}
使用 TCP/IP 协议簇的网络应用程序声明端点地址变量时,使用结构 sockaddr_in
Socket 编程-Socket API 函数
Socket API 函数(WinSock)
//WSAStartup
intWSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//WSACleanup
intWSACleanup(void);
使用 Socket 的应用程序在使用 Socket 之前必须首先调用 WSAStartup 函数
- 第一个参数指明程序请求使用的 WinSock 版本,其中高位字节指明副版本、低位字节指明主版本.
- 十六进制整数,例如 0x102 表示 2.1 版
- 第二个参数返回实际的 WinSock 的版本信息
- 指向 WSADATA 结构的指针
//使用2.1版本的WinSock的程序代码段
wVersionRequested= MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData);
应用程序在完成对请求的 Socket 库的使用,最后要调用 WSACleanup 函数,解除与 Socket 库的绑定,释放 Socket 库所占用的系统资源
sd= socket(protofamily,type,proto);
创建套接字,操作系统返回套接字描述符(sd),第一个参数(协议族): protofamily= PF_INET(TCP/IP),第二个参数(套接字类型):type = SOCK_STREAM,SOCK_DGRAM orSOCK_RAW(TCP/IP
,第三个参数(协议号):0 为默认
//创建一个流套接字的代码段
structprotoent*p;
p=getprotobyname("tcp");
SOCKET sd=socket(PF_INET,SOCK_STREAM,p->p_proto);
Socket 面向 TCP/IP 的服务类型
- TCP:可靠、面向连接、字节流传输、点对点
- UDP:不可靠、无连接、数据报传输
int closesocket(SOCKET sd);
关闭一个描述符为 sd 的套接字,如果多个进程共享一个套接字,调用 closesocket 将套接字引用计数减 1,减至 0 才关闭,一个进程中的多线程对一个套接字的使用无计数,如果进程中的一个线程调用 closesocket 将一个套接字关闭,该进程中的其他线程也将不能访问该套接字,返回值:0:成功,SOCKET_ERROR:失败
int bind(sd,localaddr,addrlen);
绑定套接字的本地端点地址:IP 地址+端口号
参数:套接字描述符(sd),端点地址(localaddr)
客户程序一般不必调用 bind 函数
服务器端:熟知端口号,IP 地址
int listen(sd,queuesize);
置服务器端的流套接字处于监听状态,仅服务器端调用,仅用于面向连接的流套接字
设置连接请求队列大小(queuesize)
connect(sd,saddr,saddrlen);
客户程序调用 connect 函数来使客户套接字(sd)与特定计算机的特定端口(saddr)的套接字(服务)进行连接,仅用于客户端,可用于 TCP 客户端也可以用于 UDP 客户端,TCP 客户端:建立 TCP 连接,UDP 客户端:指定服务器端点地址
newsock= accept(sd,caddr,caddrlen);
服务程序调用 accept 函数从处于监听状态的流套接字 sd 的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道,仅用于 TCP 套接字,仅用于服务器
利用新创建的套接字(newsock)与客户通信
send(sd,*buf,len,flags);
send 函数 TCP 套接字(客户与服务器)或调用了 connect 函数的 UDP 客户端套接字
sendto(sd,*buf,len,flags,destaddr,addrlen);
sendto 函数用于 UDP 服务器端套接字与未调用 connect 函数的 UDP 客户端套接字
recv(sd,*buffer,len,flags);
recv 函数从 TCP 连接的另一端接收数据,或者从调用了 connect 函数的 UDP 客户端套接字接收服务器发来的数据
recvfrom(sd,*buf,len,flags,senderaddr,saddrlen);
recvfrom 函数用于从 UDP 服务器端套接字与未调用 connect 函数的 UDP 客户端套接字接收对端数据
int setsockopt(intsd, intlevel, intoptname, *optval, intoptlen);
setsockopt()函数用来设置套接字 sd 的选项参数
int getsockopt(intsd, intlevel, intoptname, *optval, socklen_t*optlen);
getsockopt()函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入 optval
WSAStartup: 初始化 socket 库(仅对 WinSock)
WSACleanup: 清楚/终止 socket 库的使用(仅对 WinSock)
socket: 创建套接字 connect:“连接”远端服务器(仅用于客户端)
closesocket: 释放/关闭套接字 bind: 绑定套接字的本地 IP 地址和端口号(通常客户端不需要)
listen: 置服务器端 TCP 套接字为监听模式,并设置队列大小(仅用于服务器端 TCP 套接字)
accept: 接受/提取一个连接请求,创建新套接字,通过新套接(仅用于服务器端的 TCP 套接字)
recv: 接收数据(用于 TCP 套接字或连接模式的客户端 UDP 套接字)
recvfrom: 接收数据报(用于非连接模式的 UDP 套接字)
send: 发送数据(用于 TCP 套接字或连接模式的客户端 UDP 套接字)
sendto:发送数据报(用于非连接模式的 UDP 套接字)
setsockopt: 设置套接字选项参数
getsockopt: 获取套接字选项参数
网络字节顺序
TCP/IP 定义了标准的用于协议头中的二进制整数表示:网络字节顺序(network byte order),某些 Socket API 函数的参数需要存储为网络字节顺序(如 IP 地址、端口号等)
可以实现本地字节顺序与网络字节顺序间转换的函数
htons: 本地字节顺序 → 网络字节顺序(16bits)
ntohs: 网络字节顺序 → 本地字节顺序(16bits)
htonl: 本地字节顺序 → 网络字节顺序(32bits)
ntohl: 网络字节顺序 → 本地字节顺序(32bits)
网络应用的Socket API(TCP)调用基本流程
Socket编程-客户端软件设计
解析服务器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客户端软件流程
1.确定服务器IP地址与端口号
2.创建套接字
3.分配本地端点地址(IP地址+端口号)
4.连接服务器(套接字)
5.遵循应用层协议进行通信
6.关闭/释放连接
UDP客户端软件流程
1.确定服务器IP地址与端口号
2.创建套接字
3.分配本地端点地址(IP地址+端口号)
4.指定服务器端点地址,构造UDP数据报
5.遵循应用层协议进行通信
6.关闭/释放套接字
Socket编程-服务器软件设计
4种类型基本服务器
- 循环无连接(Iterative connectionless)服务器
- 循环面向连接(Iterative connection-oriented)服务器
- 并发无连接(Concurrent connectionless)服务器
- 并发面向连接(Concurrent connection-oriented)服务器4种类型基本服务器
循环无连接服务器基本流程
1.创建套接字
2.绑定端点地址(INADDR_ANY+端口号)
3.反复接收来自客户端的请求
4.遵循应用层协议,构造响应报文,发送给客户
数据发送:
服务器端不能使用connect()函数,无连接服务器使用sendto()函数发送数据报
获取客户端点地址:调用recvfrom()函数接收数据时,自动提取
循环面向连接服务器基本流程
1.创建(主)套接字,并绑定熟知端口号;
2.设置(主)套接字为被动监听模式,准备用于服务器;
3.调用accept()函数接收下一个连接请求(通过主套接字),创建新套接字用于与该客户建立连接;
4.遵循应用层协议,反复接收客户请求,构造并发送响应(通过新套接字);
5.完成为特定客户服务后,关闭与该客户之间的连接,返回步骤3.循环面向连接服务器基本流程
并发无连接服务器基本流程
主线程1: 创建套接字,并绑定熟知端口号;
主线程2: 反复调用recvfrom()函数,接收下一个客户请求,并创建新线程处理该客户响应;
子线程1: 接收一个特定请求;
子线程2: 依据应用层协议构造响应报文,并调用sendto()发送;
子线程3: 退出(一个子线程处理一个请求后即终止)
并发面向连接服务器基本流程
主线程1: 创建(主)套接字,并绑定熟知端口号;
主线程2: 设置(主)套接字为被动监听模式,准备用于服务器;
主线程3: 反复调用accept()函数接收下一个连接请求(通过主套接字),并创建一个新的子线程处理该客户响应;
子线程1: 接收一个客户的服务请求(通过新创建的套接字);
子线程2: 遵循应用层协议与特定客户进行交互;
子线程3: 关闭/释放连接并退出(线程终止)
例子
无连接循环DAYTIME服务器
面向连接并发DAYTIME服务器