一、需要的头文件
数据类型:#include <sys/types.h>
函数定义:#include <sys/socket.h>
TCP/IP协议族:PF_INET
TCP/IP的地址族:AF_INET
二、socke函数
int socket(int domain, int type, int protocol);
这一个函数在客户端和服务器都要使用。 它是这样被声明的:
返回值的类型与open
的相同,一个整数。 FreeBSD从和文件句柄相同的池中分配它的值。 这就是允许套接字被以对文件相同的方式处理的原因。
(1)参数domain
告诉系统你需要使用什么 协议族。有许多种协议族存在,有些是某些厂商专有的, 其它的都非常通用。协议族的声明在sys/socket.h中
使用PF_INET
是对于 UDP, TCP 和其它 网间协议(IPv4)的情况。
(2)对于参数type
有五个定义好的值,也在 sys/socket.h中。这些值都以 “SOCK_
”开头。 其中最通用的是SOCK_STREAM
, 它告诉系统你正需要一个可靠的流传送服务 (和PF_INET
一起使用时是指 TCP)提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
如果指定SOCK_DGRAM
, 你是在请求无连接报文传送服务 (在我们的情形中是UDP)数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。。
如何你需要处理基层协议 (例如IP),对较低层次协议,如IP、ICMP直接访问或者甚至是网络接口 (例如,以太网),你就需要指定 SOCK_RAW
。
(3)参数protocol
取决于前两个参数, 并非总是有意义。在以上情形中,使用取值0
。
三、Sockaddr 地址结构解析
各种各样的套接字函数需要指定地址,那是一小块内存空间 (用C语言术语是指向一小块内存空间的指针)。在 sys/socket.h中有各种各样如struct sockaddr
的声明。 这个结构是这样被声明的:
/* * 内核用来存储大多数种类地址的结构 */ struct sockaddr { u_char sa_len; /* 总长度 */ u_short sa_family; /* 地址族 */ char sa_data[14]; /* 地址值,实际可能更长 */ }; #define SOCK_MAXADDRLEN 255 /* 可能的最长的地址长度 */
sys/socket.h提到的各种类型的协议 将被按照地址族对待,并把它们就列在 sockaddr
定义的前面:
用于指定IP的是 AF_INET
。这个符号对应着常量 2
。
在sockaddr
中的域 sa_family
指定地址族, 从而决定预先只确定下大致字节数的 sa_data
的实际大小。
特别是当地址族 是AF_INET
时,我们可以使用 struct sockaddr_in
,这可在 netinet/in.h中找到,任何需要 sockaddr
的地方都以此作为实际替代。
三个重要的域是: sin_family
,结构体的字节1 1B; sin_port
,16位值,在字节2和3 2B; sin_addr
,一个32位整数,表示 IP地址,存储在字节4-7 4B。
域sin_addr
被声明为类型 struct in_addr
,这个类型定义在 netinet/in.h之中:
而in_addr_t
是一个32位整数。
假设地址192.43.244.18,这是为了表示32位整数的方便写法,按每个八位二进制字节列出, 以最高位的字节开始。
传入参数:
在不同计算机上会产生不同的效果(所谓的Big Endian和Little Endian)
Big Endian - PowerPC,Sparc64,etc
Little Endian - X86
所有网络协议都是采用Big Endian的方式来传输数据的,而Intel X86主机采用的是Little Endian,所以我们需要注意这一点
需要使用对应的转换函数
IP地址转换函数
inet_addr() 点分十进制数表示的IP地址转换为网络字节序的IP地址
inet_ntoa() 网络字节序的IP地址转换为点分十进制数表示的IP地址
字节排序函数
#include <arpa/inet.h>
or
#include <netinet/in.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);
三、客户端函数
(1)connect函数
需要头文件
#include <sys/types.h> #include <sys/socket.h>
一旦一个客户端已经建立了一个套接字, 就需要把它连接到一个远方系统的一个端口上。
参数 s
是套接字, 那是由函数socket
返回的值。 name
是一个指向 sockaddr
的指针,这个结构体我们已经展开讨论过了。 最后,namelen
通知系统 在我们的sockaddr
结构体中有多少字节。
如果 connect
成功, 返回 0
。否则返回 -1
并将错误码存放于 errno
之中。
connect函数是阻塞模式函数,除非接受到相关数据否则一直等待,类似的函数还有recvfrom和recv函数
四、一个简单的客户端程序, 一个从192.43.244.18获取当前时间并打印到 stdout的程序
五、服务器函数
典型的服务器不初始化连接。 相反,服务器等待客户端呼叫并请求服务。 服务器不知道客户端什么时候会呼叫, 也不知道有多少客户端会呼叫。服务器就是这样静坐在那儿, 耐心等待,一会儿,又一会儿, 它突然发觉自身被从许多客户端来的请求围困, 所有的呼叫都同时来到。
套接字接口提供三个基本的函数处理这种情况,bind,listen,accpet。
(1)bind函数
我们使用bind函数 告诉套接字我们要服务的端口。
Sockfd:套接字描述符,指明创建连接的套接字
my_addr:本地地址,IP地址和端口号
addrlen :地址长度
(2)Listen函数
继续我们的办公室电话类比, 在你告诉电话中心操作员你会在哪个分机后, 现在你走进你的办公室,确认你自己的电话已插上并且振铃已被打开。 还有,你确认呼叫等待功能开启,这样即使你正在与其它人通话, 也可听见电话振铃。
Sockfd:套接字描述符,指明创建连接的套接字
input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数
(3)accept函数
在你听见电话铃响后,你应答呼叫接起电话。 现在你已经建立起一个与你的客户的连接。 这个连接保持到你或你的客户挂线。
服务器通过使用函数accept函数接受连接。
注意,这次 addrlen
是一个指针。 这是必要的,因为在此情形中套接字要 填上 addr
,这是一个 sockaddr_in
结构体。
返回值是一个整数。其实, accept
返回一个 新 套接字。你将使用这个新套接字与客户通信。
老套接字会发生什么呢?它继续监听更多的请求 (想起我们传给listen
的变量 backlog
了吗?),直到我们 close
(关闭) 它。
现在,新套接字仅对通信有意义,是完全接通的。 我们不能再把它传给 listen
接受更多的连接。
Sockfd:套接字描述符,指明正在监听的套接字
addr:提出连接请求的主机地址
addrlen:地址长度
六、一个简单的服务器程序
我们开始于建立一个套接字。然后我们填好 sockaddr_in
类型的结构体 sa
。注意, INADDR_ANY
的特定使用方法:
这个常量的值是0
。由于我们已经使用 bzero
于整个结构体, 再把成员设为0
将是冗余。 但是如果我们把代码移植到其它一些 INADDR_ANY
可能不是0的系统上, 我们就需要把实际值指定给 sa.sin_addr.s_addr
。多数现在C语言 编译器已足够智能,会注意到 INADDR_ANY
是一个常量。由于它是0, 他们将会优化那段代码外的整个条件语句。
在我们成功调用bind
后, 我们已经准备好成为一个 守护进程:我们使用 fork
建立一个子进程。 同在父进程和子进程里,变量s
都是套接字。 父进程不再需要它,于是调用了close
, 然后返回0
通知父进程的父进程成功终止。
此时,子进程继续在后台工作。 它调用listen
并设置 backlog 为 4
。这里并不需要设置一个很大的值, 因为 daytime 不是个总有许多客户请求的协议, 并且总可以立即处理每个请求。
最后,守护进程开始无休止循环,按照如下步骤:
-
调用
accept
。 在这里等待直到一个客户端与之联系。在这里, 接收一个新套接字,c
, 用来与其特定的客户通信。 -
使用 C 语言函数
fdopen
把套接字从一个 低级 文件描述符 转变成一个 C语言风格的FILE
指针。 这使得后面可以使用fprintf
。 -
检查时间,按 ISO 8601格式打印到 “文件”
client
。 然后使用fclose
关闭文件。 这会把套接字一同自动关闭。
我们可把这些步骤 概括 起来, 作为模型用于许多其它服务器:
这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。
这张流程图的好处是,除了在父进程 fork
之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。
注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal
处理程序、 打开我们可能需要的文件等操作的好地方。
几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。
并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。
这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。
这张流程图的好处是,除了在父进程 fork
之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。
注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal
处理程序、 打开我们可能需要的文件等操作的好地方。
几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。
并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。