Linux 的套接字编程 (一)

来源:互联网 发布:justin和selena知乎 编辑:程序博客网 时间:2024/05/16 10:55

 

一、需要的头文件

数据类型:#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 不是个总有许多客户请求的协议, 并且总可以立即处理每个请求。

  最后,守护进程开始无休止循环,按照如下步骤:

  1. 调用accept。 在这里等待直到一个客户端与之联系。在这里, 接收一个新套接字,c, 用来与其特定的客户通信。

  2. 使用 C 语言函数 fdopen 把套接字从一个 低级 文件描述符 转变成一个 C语言风格的 FILE 指针。 这使得后面可以使用 fprintf

  3. 检查时间,按 ISO 8601格式打印到 “文件” client。 然后使用 fclose 关闭文件。 这会把套接字一同自动关闭。

  我们可把这些步骤 概括 起来, 作为模型用于许多其它服务器:

 

 

 

 

 

这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。

  这张流程图的好处是,除了在父进程 fork之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。

  注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal 处理程序、 打开我们可能需要的文件等操作的好地方。

  几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。

  并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器, 就像我们的daytime服务器能做的那样。 这只能存在于客户端与服务器没有真正的“对话”的时候: 服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。 整个操作只花费若干纳秒就完成了。

  这张流程图的好处是,除了在父进程 fork之后和父进程退出前的短暂时间内, 一直只有一个进程活跃: 我们的服务器不占用许多内存和其它系统资源。

  注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal 处理程序、 打开我们可能需要的文件等操作的好地方。

  几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。

  并非所有协议都那么简单。许多协议收到一个来自客户的请求, 回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。 这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时, 守护进程可以继续监听更多的连接。