Linux 的套接字编程 (一)

一、需要的头文件

数据类型:#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是对于 UDPTCP 和其它 网间协议(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定义的前面:

  1. /* 
  2.  * 地址族 
  3.  */  
  4. #define AF_UNSPEC       0               /* 未指定 */  
  5. #define AF_LOCAL        1               /* 本机 (管道,portal) */  
  6. #define AF_UNIX         AF_LOCAL        /* 为了向前兼容 */  
  7. #define AF_INET         2               /* 网间协议: UDP, TCP, 等等 */  
  8. #define AF_IMPLINK      3               /* arpanet imp 地址 */  
  9. #define AF_PUP          4               /* pup 协议: 例如BSP */  
  10. #define AF_CHAOS        5               /* MIT CHAOS 协议 */  
  11. #define AF_NS           6               /* 施乐(XEROX) NS 协议 */  
  12. #define AF_ISO          7               /* ISO 协议 */  
  13. #define AF_OSI          AF_ISO  
  14. #define AF_ECMA         8               /* 欧洲计算机制造商协会 */  
  15. #define AF_DATAKIT      9               /* datakit 协议 */  
  16. #define AF_CCITT        10              /* CCITT 协议, X.25 等 */  
  17. #define AF_SNA          11              /* IBM SNA */  
  18. #define AF_DECnet       12              /* DECnet */  
  19. #define AF_DLI          13              /* DEC 直接数据链路接口 */  
  20. #define AF_LAT          14              /* LAT */  
  21. #define AF_HYLINK       15              /* NSC Hyperchannel */  
  22. #define AF_APPLETALK    16              /* Apple Talk */  
  23. #define AF_ROUTE        17              /* 内部路由协议 */  
  24. #define AF_LINK         18              /* 协路层接口 */  
  25. #define pseudo_AF_XTP   19              /* eXpress Transfer Protocol (no AF) */  
  26. #define AF_COIP         20              /* 面向连接的IP, 又名 ST II */  
  27. #define AF_CNT          21              /* Computer Network Technology */  
  28. #define pseudo_AF_RTIP  22              /* 用于识别RTIP包 */  
  29. #define AF_IPX          23              /* Novell 网间协议 */  
  30. #define AF_SIP          24              /* Simple 网间协议 */  
  31. #define pseudo_AF_PIP   25              /* 用于识别PIP包 */  
  32. #define AF_ISDN         26              /* 综合业务数字网(Integrated Services Digital Network) */  
  33. #define AF_E164         AF_ISDN         /* CCITT E.164 推荐 */  
  34. #define pseudo_AF_KEY   27              /* 内部密钥管理功能 */  
  35. #define AF_INET6        28              /* IPv6 */  
  36. #define AF_NATM         29              /* 本征ATM访问 */  
  37. #define AF_ATM          30              /* ATM */  
  38. #define pseudo_AF_HDRCMPLT 31           /* 由BPF使用,就不必在接口输出例程  
  39.                                          * 中重写头文件了  
  40.                                          */  
  41. #define AF_NETGRAPH     32              /* Netgraph 套接字 */  
  42. #define AF_SLOW         33              /* 802.3ad 慢速协议 */  
  43. #define AF_SCLUSTER     34              /* Sitara 集群协议 */  
  44. #define AF_ARP          35  
  45. #define AF_BLUETOOTH    36              /* 蓝牙套接字 */  
  46. #define AF_MAX          37  
 

 

用于指定IP的是 AF_INET。这个符号对应着常量 2

  在sockaddr中的域 sa_family指定地址族, 从而决定预先只确定下大致字节数的 sa_data的实际大小。

  特别是当地址族 是AF_INET时,我们可以使用 struct sockaddr_in,这可在 netinet/in.h中找到,任何需要 sockaddr的地方都以此作为实际替代。

  1. /* 
  2.  * 套接字地址,Internet风格 
  3.  */  
  4. struct sockaddr_in {  
  5.     uint8_t     sin_len;  
  6.     sa_family_t sin_family;  
  7.     in_port_t   sin_port;  
  8.     struct  in_addr sin_addr;  
  9.     char    sin_zero[8];  
  10. };  
 

 

三个重要的域是: 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之中:

  1. /* 
  2.  * Internet 地址 (由历史原因而形成的结构) 
  3.  */  
  4. struct in_addr {  
  5.     in_addr_t s_addr;  
  6. };  
 

 

in_addr_t是一个32位整数。

  假设地址192.43.244.18,这是为了表示32位整数的方便写法,按每个八位二进制字节列出, 以最高位的字节开始。

传入参数:

  1. sa.sin_family      = AF_INET;  
  2.     sa.sin_port        = 13;  
  3.     sa.sin_addr.s_addr = (((((192 << 8) | 43) << 8) | 244) << 8) | 18;  
 

在不同计算机上会产生不同的效果(所谓的Big Endian和Little Endian)

 

Big Endian - PowerPC,Sparc64,etc

Little Endian - X86

  1. 【用函数判断系统是Big Endian还是Little Endian】  
  2. bool IsBig_Endian()  
  3. //如果字节序为big-endian,返回true;  
  4. //反之为   little-endian,返回false  
  5. {  
  6.     unsigned short test = 0x1122;  
  7.     if(*( (unsigned char*) &test ) == 0x11)  
  8.        return TRUE;  
  9. else  
  10.     return FALSE;  
  11. }//IsBig_Endian()  
 

所有网络协议都是采用Big Endian的方式来传输数据的,而Intel X86主机采用的是Little Endian,所以我们需要注意这一点

需要使用对应的转换函数

 

 

IP地址转换函数

inet_addr() 点分十进制数表示的IP地址转换为网络字节序的IP地址

inet_ntoa() 网络字节序的IP地址转换为点分十进制数表示的IP地址

 

字节排序函数

 

#include <arpa/inet.h> or #include <netinet/in.huint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_thostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);

 

 

三、客户端函数

(1)connect函数

需要头文件

 

#include <sys/types.h#include <sys/socket.h>

 

一旦一个客户端已经建立了一个套接字, 就需要把它连接到一个远方系统的一个端口上。

  1. int connect(int s, const struct sockaddr *name, socklen_t namelen);  
 

 

参数 s 是套接字, 那是由函数socket返回的值。 name 是一个指向 sockaddr的指针,这个结构体我们已经展开讨论过了。 最后,namelen通知系统 在我们的sockaddr结构体中有多少字节。

  如果 connect 成功, 返回 0。否则返回 -1 并将错误码存放于 errno之中。

 

connect函数是阻塞模式函数,除非接受到相关数据否则一直等待,类似的函数还有recvfrom和recv函数

 

四、一个简单的客户端程序, 一个从192.43.244.18获取当前时间并打印到 stdout的程序

  1. /* 
  2.  * daytime.c 
  3.  * 
  4.  * G. Adam Stanislav 编程 
  5.  */  
  6. #include <stdio.h>  
  7. #include <string.h>  
  8. #include <sys/types.h>  
  9. #include <sys/socket.h>  
  10. #include <netinet/in.h>  
  11. int main() {  
  12.   register int s;  
  13.   register int bytes;  
  14.   struct sockaddr_in sa;  
  15.   char buffer[BUFSIZ+1];  
  16.   if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {  
  17.     perror("socket");  
  18.     return 1;  
  19.   }  
  20.   bzero(&sa, sizeof sa);  
  21.   sa.sin_family = AF_INET;  
  22.   sa.sin_port = htons(13);  
  23.   sa.sin_addr.s_addr = htonl((((((192 << 8) | 43) << 8) | 244) << 8) | 18);  
  24.   if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {  
  25.     perror("connect");  
  26.     close(s);  
  27.     return 2;  
  28.   }  
  29.   while ((bytes = read(s, buffer, BUFSIZ)) > 0)  
  30.     write(1, buffer, bytes);  
  31.   close(s);  
  32.   return 0;  
  33. }  
 

 

五、服务器函数

 

      典型的服务器不初始化连接。 相反,服务器等待客户端呼叫并请求服务。 服务器不知道客户端什么时候会呼叫, 也不知道有多少客户端会呼叫。服务器就是这样静坐在那儿, 耐心等待,一会儿,又一会儿, 它突然发觉自身被从许多客户端来的请求围困, 所有的呼叫都同时来到。

  套接字接口提供三个基本的函数处理这种情况,bind,listen,accpet。

(1)bind函数

 

 我们使用bind函数 告诉套接字我们要服务的端口。

  1. int bind(int s, const struct sockaddr *addr, socklen_t addrlen);  
 

Sockfd:套接字描述符,指明创建连接的套接字

my_addr:本地地址,IP地址和端口号

addrlen :地址长度

 

(2)Listen函数

继续我们的办公室电话类比, 在你告诉电话中心操作员你会在哪个分机后, 现在你走进你的办公室,确认你自己的电话已插上并且振铃已被打开。 还有,你确认呼叫等待功能开启,这样即使你正在与其它人通话, 也可听见电话振铃。

  1. int listen(int s, int backlog);  

Sockfd:套接字描述符,指明创建连接的套接字

input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数 

 

(3)accept函数

 

在你听见电话铃响后,你应答呼叫接起电话。 现在你已经建立起一个与你的客户的连接。 这个连接保持到你或你的客户挂线。

  服务器通过使用函数accept函数接受连接。

 

 

 

  1. int accept(int s, struct sockaddr *addr, socklen_t *addrlen);  
 

注意,这次 addrlen 是一个指针。 这是必要的,因为在此情形中套接字要 填上 addr,这是一个 sockaddr_in 结构体。

  返回值是一个整数。其实, accept 返回一个 新 套接字。你将使用这个新套接字与客户通信。

  老套接字会发生什么呢?它继续监听更多的请求 (想起我们传给listen的变量 backlog了吗?),直到我们 close(关闭) 它。

  现在,新套接字仅对通信有意义,是完全接通的。 我们不能再把它传给 listen接受更多的连接。

 

Sockfd:套接字描述符,指明正在监听的套接字

addr:提出连接请求的主机地址

addrlen:地址长度

 

 

六、一个简单的服务器程序

 

  1. /* 
  2.  * daytimed - 端口 13 的服务器 
  3.  * 
  4.  * G. Adam Stanislav 编程 
  5.  * 2001年6月19日 
  6.  */  
  7. #include <stdio.h>  
  8. #include <string.h>  
  9. #include <time.h>  
  10. #include <unistd.h>  
  11. #include <sys/types.h>  
  12. #include <sys/socket.h>  
  13. #include <netinet/in.h>  
  14. #define BACKLOG 4  
  15. int main() {  
  16.     register int s, c;  
  17.     int b;  
  18.     struct sockaddr_in sa;  
  19.     time_t t;  
  20.     struct tm *tm;  
  21.     FILE *client;  
  22.     if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {  
  23.         perror("socket");  
  24.         return 1;  
  25.     }  
  26.     bzero(&sa, sizeof sa);  
  27.     sa.sin_family = AF_INET;  
  28.     sa.sin_port   = htons(13);  
  29.     if (INADDR_ANY)  
  30.         sa.sin_addr.s_addr = htonl(INADDR_ANY);  
  31.     if (bind(s, (struct sockaddr *)&sa, sizeof sa) < 0) {  
  32.         perror("bind");  
  33.         return 2;  
  34.     }  
  35.     switch (fork()) {  
  36.         case -1:  
  37.             perror("fork");  
  38.             return 3;  
  39.             break;  
  40.         default:  
  41.             close(s);  
  42.             return 0;  
  43.             break;  
  44.         case 0:  
  45.             break;  
  46.     }  
  47.     listen(s, BACKLOG);  
  48.     for (;;) {  
  49.         b = sizeof sa;  
  50.         if ((c = accept(s, (struct sockaddr *)&sa, &b)) < 0) {  
  51.             perror("daytimed accept");  
  52.             return 4;  
  53.         }  
  54.         if ((client = fdopen(c, "w")) == NULL) {  
  55.             perror("daytimed fdopen");  
  56.             return 5;  
  57.         }  
  58.         if ((t = time(NULL)) < 0) {  
  59.             perror("daytimed time");  
  60.             return 6;  
  61.         }  
  62.         tm = gmtime(&t);  
  63.         fprintf(client, "%.4i-%.2i-%.2iT%.2i:%.2i:%.2iZ/n",  
  64.             tm->tm_year + 1900,  
  65.             tm->tm_mon + 1,  
  66.             tm->tm_mday,  
  67.             tm->tm_hour,  
  68.             tm->tm_min,  
  69.             tm->tm_sec);  
  70.         fclose(client);  
  71.     }  
  72. }  
 

我们开始于建立一个套接字。然后我们填好 sockaddr_in 类型的结构体 sa。注意, INADDR_ANY的特定使用方法:

  1. if (INADDR_ANY)  
  2.         sa.sin_addr.s_addr = htonl(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 是个例外,我们考虑为一个 “黑盒子”, 那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值