Loading...
墨滴

阿酷尔工作室

2021/10/16  阅读:31  主题:默认主题

Beej网络编程指南

Beej网络编程指南《一》

使用Internet套接字

布赖恩"Beej Jorgensen"霍尔

V3.1.5,版权©2020年11月20日

1简介

嘿!套接字编程让你失望了吗这东西是不是有点太难从手册页上弄清楚了?你想做很酷的互联网编程,但是你没有时间费力地穿过一堆结构,试图弄清楚在连接()之前是否必须调用bind(),等等。等等。等等。

嗯,你猜怎么着!我已经做过这个讨厌的事情了,我非常想和每个人分享这个信息!你来对地方了。这个文档应该给普通的有能力的C程序员一个控制网络噪音的优势。

看看吧:我终于赶上了未来(也是在关键时刻!)并更新了IPv6指南!享受吧!

1.1受众

本文档是作为教程编写的,不是完整的参考。当刚刚开始套接字编程并正在寻找立足点的个人阅读时,它可能是最好的。无论如何,它肯定不是套接字编程的完整指南。

不过,希望这足以让那些手册开始有意义...:-

1.2平台和编译器

本文档中包含的代码是使用Gnu的gcc编译器在Linux电脑上编译的。然而,它应该建立在几乎任何使用gcc的平台上。自然,如果您为视窗编程,这并不适用——请参阅下面``关于视窗编程的部分。

1.3官方主页和图书出售

本文件的官方位置是:

  • https://beej.us/guide/bgnet/

在那里,您还可以找到示例代码和指南翻译成各种语言。

要购买装订精美的印刷品(有些人称之为“书籍”),请访问:

  • https://beej.us/guide/url/bgbuy

我很感激这次购买,因为它有助于维持我的文档写作生活方式!

1.4 Solaris/SunOS程序员注意事项

当为Solaris或SunOS编译时,您需要指定一些额外的命令行开关,以便在适当的库中进行链接。为了做到这一点,只需在编译命令的末尾添加“-lnsl-lSocococe-lresrev”,如下所示:

    $ cc -o server server.c -lnsl -lsocket -lresolv

如果仍然出现错误,您可以尝试在命令行的末尾添加-lxnet。我不知道具体是什么,但是有些人似乎需要它。

您可能会发现问题的另一个地方是调用setsokopt()。原型与我的Linux盒子上的不同,所以不是:

    int yes=1;

输入此:

    char yes='1';

由于我没有太阳盒,我没有测试上述任何信息——这只是人们通过电子邮件告诉我的。

1.5 Windows程序员注意事项

在指南的这一点上,从历史上看,我在视窗上做了一点打包,仅仅是因为我不太喜欢它。但是我应该公平地告诉你视窗有一个巨大的安装基础,显然是一个非常好的操作系统。

他们说离开会让心变得更好,在这种情况下,我相信这是真的。(或者可能是年龄的问题。)但我能说的是,在十多年没有在我的个人工作中使用微软操作系统后,我快乐多了!因此,我可以坐下来放心地说,“当然,请随意使用视窗系统!”...好吧,是的,这确实让我咬紧牙关说出来。

因此,我仍然鼓励您尝试Linux1BSD2或Unix的一些风格。

但是人们喜欢他们喜欢的,你们视窗人会很高兴知道这些信息通常适用于你们,如果有的话,会有一些小的变化。

你可以做的一件很酷的事情是安装Cygwin3,这是一个用于视窗的Unix工具的集合。我听说这样做可以让所有这些程序未经修改地编译。

你应该考虑的另一件事是Linux4的视窗子系统这基本上允许你在视窗10上安装一个类似Linux虚拟机的东西。这也肯定会让你定位。

但是你们中的一些人可能想用纯视窗方式做事。你太勇敢了,这就是你必须做的:马上跑出去买Unix!不,不——我开玩笑的。这些天我应该是视窗友好的...

这是你必须做的(除非你安装Cygwin!):首先,忽略我在这里提到的几乎所有系统头文件。你需要包括的是:

    #include <winsock.h>

等等!在对套接字库做任何其他操作之前,您还必须调用WSAStartup()。执行此操作的代码如下所示:

#include <winsock.h>

{
    WSADATA wsaData;   // if this doesn't work
    //WSAData wsaData; // then try this instead

    // MAKEWORD(1,1) for Winsock 1.1, MAKEWORD(2,0) for Winsock 2.0:

    if (WSAStartup(MAKEWORD(1,1), &wsaData) != 0) {
        fprintf(stderr"WSAStartup failed.\n");
        exit(1);
    }

您还必须告诉您的编译器在Winsock库中链接,通常称为wsock32.libwinsock32.lib,或ws2_32.libWinsock 2.0。在VC++下,这可以通过项目菜单中的设置......单击链接选项卡,并查找标题为“对象/库模块”的框。将“wsock32.lib”(或您喜欢的任何库)添加到该列表中。

我听说是这样。

最后,当您完成套接字库时,您需要调用WSACleanup()。有关详细信息,请参阅您的在线帮助。

一旦您这样做了,本教程中的其余示例应该会普遍适用,只有少数例外。首先,您不能使用关闭(来关闭套接字——您需要使用关闭()来代替。此外,选择()只适用于套接字描述符,而不适用于文件描述符(如标准输入值为0)。

还有一个可以使用的套接字类,CSocket。查看编译器帮助页面了解更多信息。

要获得有关Winsock的更多信息,请阅读Winsock常见问题5并从那里开始。

最后,我听说Windows没有fork()系统调用,不幸的是,在我的一些例子中使用了这个调用。也许你必须在POSIX库或其他地方链接才能让它工作,或者你可以使用CreateProcess()代替。fork()不需要参数,而CreateProcess()需要大约480亿参数。如果你不具备这一点,CreateThread()更容易消化...不幸的是,关于多线程的讨论超出了本文档的范围。我只能说这么多,你知道!

1.6电子邮件政策

我通常可以帮助解决电子邮件问题,所以请随意写信,但我不能保证得到回复。我过着相当忙碌的生活,有时我就是不能回答你的问题。在这种情况下,我通常只是删除邮件。这不是针对个人的;我只是永远没有时间给出你需要的详细答案。

通常情况下,问题越复杂,我回答的可能性就越小。如果你能在邮寄前缩小问题的范围,并确保包含任何相关信息(如平台、编译器、你收到的错误信息,以及任何你认为可能有助于我排除故障的其他信息),你就更有可能得到回应。更多建议,请阅读ESR的文档《如何提问智能之6》。

如果你没有得到回应,再破解一下,试着找到答案,如果仍然难以捉摸,那就用你找到的信息再给我写信,希望这足以让我帮忙。

既然我已经纠缠你如何写和不写我,我只想让你知道,我完全感谢导游多年来得到的所有赞扬。这真是鼓舞士气,听到它被永远使用,我很高兴!:-)谢谢你!

1.7镜像

欢迎您公开或私下镜像本网站。如果您公开镜像该网站,并希望我从主页链接到它,请在beej@beej.us给我留言。

1.8翻译注意事项

如果您想将指南翻译成另一种语言,请在beej@beej.us写信给我,我会从主页链接到您的翻译。请随时将您的姓名和联系信息添加到翻译中。

此源标记文档使用UTF-8编码。

请注意下面版权、发行和法律部分中的许可限制。

如果你想让我主持翻译,只要问。如果你想主持,我也会链接到它;任何一种方式都可以。

1.9版权、发行和法律

毕杰网络编程指南版权所有©2019布莱恩“毕杰·乔根森”大厅。

除了源代码和翻译的特殊例外,下面,本作品是根据知识共享署名-非商业性-无衍生作品3.0许可证获得许可的。要查看此许可证的副本,请访问

https://creativecommons.org/licenses/by-nc-nd/3.0/

或者给美国加利福尼亚州旧金山市第二街171号300套房的知识共享空间发一封信,邮编: 94105。

许可证中“禁止衍生作品”部分的一个具体例外如下:本指南可以自由翻译成任何语言,前提是翻译准确,并且指南全文转载。翻译和原始指南适用相同的许可限制。翻译还可能包括翻译者的姓名和联系信息。

本文档中提供的C源代码特此授予公共域,并且完全不受任何许可限制。

教育工作者可以自由地向他们的学生推荐或提供本指南的副本。

除非双方另有书面约定,作者按原样提供作品,并且不对作品做出任何形式的陈述或保证,无论是明示的、暗示的、法定的还是其他的,包括但不限于所有权、适销性、特定用途的适用性、不侵权、不存在潜在或其他缺陷、准确性或不存在错误,无论是否可发现。

除适用法律要求的范围外,在任何情况下,作者都不会根据任何法律理论对因使用作品而产生的任何特殊、偶然、后果性、惩罚性或惩戒性损害承担责任,即使作者已被告知此类损害的可能性。

联系beej@beej.us了解更多信息。

1.10奉献

感谢所有在过去和未来帮助我编写这本指南的人。感谢所有制作我用来制作指南的自由软件和包的人: GNU、Linux、Slackware、vim、Python、Inkscape、pandoc等。最后,非常感谢成千上万写信给我的人,他们提出了改进建议和鼓励的话。

我把这本指南献给我在计算机世界中的一些最大的英雄和侵略者:唐纳德·克努特、布鲁斯·施奈尔、理查德·史蒂文斯和沃兹、我的读者以及整个自由和开源软件社区。

1.11出版信息

这本书是在装有GNU工具的Arch Linux盒子上使用vim编辑器用Markdown编写的。封面“艺术”和图表是用Inkscape制作的。Markdown被Python、Pandoc和XeLaTeX转换为超文本标记语言和LaTex/PDF,使用解放字体。工具链由100%免费和开源软件组成。

2什么是插座?

你总是听到“套接字”的说法,也许你想知道它们到底是什么。嗯,它们是这样的:一种使用标准Unix文件描述符与其他程序对话的方式。

什么啊?

好的——你可能听说过一些Unix黑客状态,“天哪,Unix中的一切文件!”那个人可能一直在谈论这样一个事实,当Unix程序执行任何类型的输入/输出时,他们都是通过读取或写入文件描述符来完成的。文件描述符只是一个与打开的文件相关联的整数。但是(这里有个问题),那个文件可以是网络连接、FIFO、管道、终端、真正的磁盘文件,或者其他任何东西。Unix中的一切都是一个文件!所以当你想通过互联网与另一个程序通信时,你最好相信它是通过文件描述符来实现的。

"Smarty-Pants先生,我从哪里得到这个用于网络通信的文件描述符?"可能是您现在想到的最后一个问题,但我还是要回答它:您调用套接字()系统例程。它返回套接字描述符,您通过它使用专门的发送()和recv()(man发送,man recv)套接字调用进行通信。

“但是,嘿!”你现在可能会惊呼。“如果它是一个文件描述符,为什么以海王星的名义,我不能使用正常的read()和Writ()调用通过套接字进行通信?”简短的回答是,“你可以!”更长的回答是,“你可以,但是mail()和recv()对你的数据传输提供了更大的控制。”

接下来呢?这个怎么样:有各种各样的套接字。有DARPA互联网地址(互联网套接字)、本地节点上的路径名称(Unix套接字)、CCITT X.25地址(您可以安全忽略的X.25套接字),可能还有许多其他地址,这取决于您运行的是哪种Unix风格。本文档只处理第一个:互联网套接字。

2.1两种类型的Internet套接字

这是什么?有两种类型的Internet套接字?是的。嗯,不是。我在撒谎。还有更多,但我不想吓到你。我在这里只讲两种类型。除了这句话,我在哪里要告诉你“原始套接字”也很强大,你应该去查一下。

好的,已经有两种类型了。一种是流套接字;另一种是数据报套接字,以后可以分别称为SOCK_STREAM和SOCK_DGRAM。数据报套接字有时被称为无连接套接字。(尽管如果您真的需要,它们可以是连接()'d。请参见下面的连接()。)

流套接字是可靠的双向连接通信流。如果你按“1,2”的顺序向套接字输出两个项目,它们将按“1,2”的顺序到达相反的一端。它们也不会出错。我非常确定,事实上,它们不会出错,如果有人试图声称不是这样,我就把手指放在耳朵里,高呼啦啦啦啦

什么使用流套接字?嗯,你可能听说过telnet应用程序,对吗?它使用流套接字。你键入的所有字符都需要按照你键入它们的顺序到达,对吗?此外,网络浏览器使用超文本传输协议(HTTP),它使用流套接字来获取页面。事实上,如果你在端口80上远程访问一个网站,键入“GET/HTTP/1.0”并点击“返回”两次,它会把超文本标记语言转回给你!

如果您没有安装telnet并且不想安装它,或者您的telnet对连接到客户端很挑剔,指南附带了一个类似telnet的程序,名为telno``t7。这应该能很好地满足指南的所有需求。(请注意,telnet实际上是一个规范的网络协议,telnot根本不实现这个协议。)

流套接字如何实现如此高水平的数据传输质量?它们使用一种称为“传输控制协议”的协议,也称为“TCP”(有关TCP的极其详细的信息,请参阅RFC 7939)。TCP确保您的数据按顺序无误地到达。您可能以前听说过“TCP”是“TCP/IP”的另一半,其中“IP”代表“网络之间互连的协议”(请参阅RFC 79110。IP主要处理互联网路由,通常不负责数据完整性。

酷。数据报套接字呢?为什么它们被称为无连接?到底是怎么回事?为什么它们不可靠?这里有一些事实:如果你发送数据报,它可能会到达。它可能会乱序到达。如果它到达,数据包中的数据将是无错误的。

数据报套接字也使用IP进行路由,但它们不使用TCP;它们使用用户数据报协议或UDP(请参阅RFC 76811

为什么它们是无连接的?嗯,基本上,这是因为你不必像处理流套接字那样保持开放的连接。你只需构建一个数据包,在上面输入一个带有目的地信息的IP标头,然后发送出去。不需要连接。它们通常用于TCP堆栈不可用的情况,或者偶尔丢弃几个数据包并不意味着宇宙的终结。示例应用程序:tftp(普通文件传输协议,FTP的小兄弟)、dhcpcd(DHCP客户端)、多人游戏、流媒体音频、视频会议等。

"等一下!tftpdhcpcd用于将二进制应用程序从一个主机传输到另一个主机!如果你期望应用程序在到达时工作,数据不会丢失!这是什么样的黑暗魔法?"

嗯,我的人类朋友,tftp和类似的程序在UDP之上有自己的协议。例如,tftp协议规定,对于发送的每个数据包,接收者必须发回一个写着“我明白了!”(“ACK”数据包)的数据包。如果原始数据包的发送者在五秒钟内没有得到回复,他将重新发送数据包,直到他最终得到一个ACK。在实现可靠的SOCK_DGRAM应用程序时,这种确认过程非常重要。

对于游戏、音频或视频等不可靠的应用程序,你可以忽略掉的数据包,或者尝试巧妙地补偿它们。(地震玩家会通过技术术语“诅咒的滞后”知道这种效果的表现。在这种情况下,“诅咒”一词代表任何极其亵渎的话语。)

你为什么要使用一个不可靠的底层协议?有两个原因:速度和速度。比起跟踪安全到达的东西并确保它是有序的,发射后忘记要快得多。如果你发送聊天信息,TCP很棒;如果你每秒发送40个玩家的位置更新,也许一两个被丢弃并不重要,UDP是一个不错的选择。

2.2低级废话与网络理论

既然我刚刚提到了协议的分层,现在是时候讨论网络是如何真正工作的了,并且展示一些如何构建SOCK_DGRAM包的例子。实际上,您可能可以跳过这一部分。然而,这是很好的背景。

数据封装。

嘿,孩子们,是时候学习数据封装了!这非常非常重要。这非常重要,如果你在奇科州立大学学习网络课程,你可能会学到它;-)。基本上,它是这样说的:一个包诞生了,这个包被第一个协议(比如TFTP协议)包装(封装)在一个报头(很少是页脚)中,然后整个东西(包括TFTP报头)被下一个协议(比如UDP)再次封装,然后被下一个(IP)再次封装,然后被硬件(物理)层的最终协议(比如以太网)再次封装。

当另一台计算机收到数据包时,硬件剥离以太网报头,内核剥离IP和UDP报头,TFTP程序剥离TFTP报头,它最终拥有数据。

现在我终于可以谈谈臭名昭著的分层网络模型(又名“国际标准化组织/现场视察”)了。这个网络模型描述了一个网络功能系统,它比其他模型有许多优势。例如,你可以编写完全相同的套接字程序,而不关心数据是如何物理传输的(串行、薄以太网、AUI等等),因为较低级别的程序会为你处理它。实际的网络硬件和拓扑对套接字程序员来说是透明的。

闲话少说,我将展示完整模型的各层。网络类考试记住这一点:

  • 应用程序

  • 演示文稿

  • 届会

  • 交通工具

  • 网络

  • 数据链接

  • 物理的

物理层是硬件(串行、以太网等)。)应用层离物理层只有你能想象的那么远——它是用户与网络交互的地方。

现在,这个模型非常通用,如果你真的想的话,你可以把它作为汽车维修指南。一个与Unix更一致的分层模型可能是:

  • 应用层*(telnet,ftp等*)

  • 主机到主机传输层*(TCP, UDP)*

  • 互联网层(IP和路由

  • 网络接入层(以太网、Wi-Fi或其他

此时,您可能可以看到这些层如何对应于原始数据的封装。

看看构建一个简单的数据包要做多少工作?天哪!你必须用“cat”自己输入数据包标题!开玩笑的。对于流套接字,你所要做的就是发送()数据出去。对于数据报套接字,你所要做的就是用你选择的方法封装数据包,然后发送()出去。内核为你构建了传输层和互联网层,硬件为网络访问层。啊,现代技术。

我们对网络理论的短暂探索就这样结束了。哦,是的,我忘了告诉你我想说的关于路由的一切:没什么!没错,我根本不打算谈论它。路由器将数据包剥离到IP头,查阅它的路由表,诸如此类。如果你真的在乎,看看*IP RFC12。如果你永远不了解它,那么你会活下去。*

3 IP地址、结构和数据处理

这是游戏的一部分,我们可以讨论改变的代码。

但是首先,让我们讨论更多的非代码!耶。首先,我想稍微谈谈IP地址和端口,这样我们就可以解决这个问题了。然后我们将讨论套接字应用编程接口如何存储和操作IP地址和其他数据。

3.1 IP地址,版本4和6

在过去的好时光里,当本·克诺比还被称为欧比·万·克诺比时,有一个很棒的网络路由系统,叫做网络之间互连的协议版本4,也叫IPv4。它的地址由四个字节组成(也就是四个“八位字节”),通常用“点和数字”的形式书写,比如:192.0.2.111

你可能在附近见过。

事实上,截至本文撰写之时,互联网上几乎每个网站都使用IPv4。

每个人,包括欧比万,都很开心。事情很顺利,直到一个叫温特·瑟夫的反对者警告每个人,我们的IPv4地址即将用完!

(除了警告所有人即将到来的IPv4末日与黑暗启示录,文特·瑟夫13还以互联网之父而闻名。所以我真的没有资格猜测他的判断。)

地址用完了?怎么会这样?我是说,一个32位IPv4地址有数十亿个IP地址。我们真的有数十亿台电脑吗?

是的。

此外,在一开始,当只有几台电脑,每个人都认为10亿是一个不可思议的大数字时,一些大组织慷慨地分配了数百万个IP地址供自己使用。(例如施乐、麻省理工学院、福特、惠普、IBM、通用电气、AT&T和一些叫苹果的小公司)。)

其实要不是有几个权宜之计,我们早就用完了。

但是现在我们生活在一个时代,我们谈论的是每个人都有一个知识产权地址,每一台电脑,每一台计算器,每一部电话,每一个停车计时器,还有(为什么不呢)每一只小狗。

因此,IPv6诞生了。由于温特·瑟夫可能是不朽的(即使他的身体形态应该会传承下去,但愿不会,他可能已经作为某种超智能ELIZA14程序存在于互联网2的深处),如果我们在下一个版本的网络之间互连的协议中没有足够的地址,没有人想听到他再说一遍“我告诉过你”。

这对你意味着什么?

我们需要更的地址。我们需要的不仅仅是两倍的地址,不是十亿倍的地址,不是一千万亿倍的地址,而是7900万倍的地址!这会让他们明白的

你在说,“Beej,是真的吗?我完全有理由不相信大数。”嗯,32位和128位之间的差异听起来可能并不大;它只是多了96位,对吗?但是请记住,我们在这里谈论的是能量: 32位代表一些40亿数字(232),而128位代表大约340万亿万亿个数字(真的是2128)。这就像宇宙中每一颗恒星都有一百万个IPv4互联网。

忘记IPv4的点和数字外观;现在我们有了十六进制表示,每个两字节块由冒号分隔,像这样:

    2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551

这还不是全部!很多时候,你会有一个包含很多零的IP地址,你可以在两个冒号之间压缩它们。你可以为每对字节去掉前导零。例如,每对地址都是等价的:

    2001:0db8:c9d2:0012:0000:0000:0000:0051    2001:db8:c9d2:12::51        2001:0db8:ab00:0000:0000:0000:0000:0000    2001:db8:ab00::        0000:0000:0000:0000:0000:0000:0000:0001    ::1

::1 地址是环回地址。它总是意味着“我现在运行的这台机器”。在IPv4中,环回地址127.0.0.1

最后,您可能会遇到IPv6地址的IPv4兼容性模式。例如,如果您想将IPv4地址192.0.2.33表示为IPv6地址,您可以使用以下符号:“::ffff:192.0.2.33”

我们在谈论严肃的乐趣。

事实上,这真的很有趣,IPv6的创造者非常傲慢地砍掉了数万亿个地址用于保留用途,但是坦率地说,我们有这么多地址,谁还在数呢?银河系中每个星球上的每个男人、女人、孩子、小狗和停车收费表都有很多剩余的。相信我,银河系中的每个星球都有停车收费表。你知道这是真的。

3.1.1子网

出于组织原因,有时很方便地声明“这个IP地址的第一部分通过这个位是IP地址的网络部分,其余部分是主机部分

例如,对于IPv4,你可能192.0.2.12,我们可以说前三个字节是网络,最后一个字节是主机。或者,换句话说,我们在网络192.0.2.0上谈论主机12(看看我们如何将主机的字节归零)。

现在来看更多过时的信息!准备好了吗?在古代,子网有“类”,地址的第一个、两个或三个字节是网络部分。如果你足够幸运,网络有一个字节,主机有三个字节,你的网络上可能有24位的主机(1600万左右)。这就是“A类”网络。另一端是“C类”,有三个字节的网络和一个字节的主机(256个主机,减去两个预留的主机)。

如你所见,只有几个A类,一大堆C类,中间还有一些B类。

IP地址的网络部分由一种叫做网络掩码的东西来描述,你可以用IP地址进行逐位和运算,从中获得网络号。网络掩码通常看起来像255.255.255.0。(例如,有了网络掩码,如果你的IP192.0.2.12,那么你的网络192.0.2.12255.255.255.0这给了192.0.2.0。)

不幸的是,事实证明这对于互联网的最终需求来说还不够细粒度;我们很快就用完了C类网络,而且我们肯定已经用完了A类网络,所以不用问了。为了补救这一点,网络掩码允许的功率是任意位数,而不仅仅是8、16或24。所以你可能有一个网络掩码,比如说255.255.255.252,它是30位网络,2位主机允许网络上的4台主机。(请注意,网络掩码总是一堆1位后面跟着一堆0位。)

但是使用像255.192.0.0这样的一大串数字作为网络掩码有点笨拙。首先,人们对这是多少比特没有直观的概念,其次,它真的不紧凑。所以新样式出现了,它好多了。你只需要在IP地址后面放一个斜杠,然后在斜杠后面跟着十进制的网络比特数。像这样:192.0.2.12/30

或者,对于IPv6,类似这样的东西:2001: db8::/32或2001: db8:5413:4028::9db9/64。

3.1.2端口号

如果你还记得的话,我之前向你介绍了分层网络模型,它将互联网层(IP)从主机到主机传输层(TCP和UDP)分离出来。在下一段之前加快速度。

事实证明,除了IP地址(由IP层使用),还有另一个地址被TCP(流套接字)使用,巧合的是,被UDP(数据报套接字)使用。它是端口号。这是一个16位的数字,类似于连接的本地地址。

把IP地址想象成酒店的街道地址,把端口号想象成房间号。这是一个不错的类比;也许以后我会想出一个涉及汽车行业的例子。

假设你想要一台处理传入邮件和网络服务的计算机——在一台只有一个IP地址的计算机上,你如何区分这两者?

互联网上的不同服务有不同的众所周知的端口号。你可以在大IANA端口列表15中看到它们,或者,如果你在Unix框中,在你的 /etc/services文件中看到它们。HTTP(网络)是端口80,telnet是端口23,SMTP是端口25,游戏DOOM16使用的端口666等等。1024以下的端口通常被认为是特殊的,通常需要特殊的操作系统特权才能使用。

就这样!

3.2字节顺序

根据王国的命令!将有两个字节的顺序,此后被称为跛脚和壮丽!

我开玩笑,但一个真的比另一个好。:-

说起来真的不容易,所以我脱口而出:你的电脑可能在你背后以相反的顺序存储字节。我知道!没人想告诉你。

问题是,互联网世界中的每个人都普遍同意,如果你想表示两个字节的十六进制数,比如b34f,你将把它存储在两个连续的字节b34f中。这是有道理的,正如威尔福德·布里姆利17会告诉你的那样,这是正确的事情。这个数字,先和大端存储在一起,叫做大端

不幸的是,散布在世界各地的一些计算机,即任何具有英特尔或英特尔兼容处理器的计算机,都存储相反的字节,因此*b34f*将作为顺序字节4f后跟b3存储在内存中。这种存储方法被称为Little-Endian。

但是等等,我还没有完成术语!更理智的大端也被称为网络字节顺序,因为这是我们网络类型喜欢的顺序。

您的计算机以主机字节顺序存储数字。如果是英特尔80x86,主机字节顺序是小端。如果是摩托罗拉68k,主机字节顺序是大端。如果是PowerPC,主机字节顺序是......嗯,这取决于!

很多时候,当你构建数据包或填写数据结构时,你需要确保你的两个和四个字节的数字是网络字节顺序的。但是如果你不知道本机主机字节顺序,你怎么能做到这一点呢?

好消息!您只需假设主机字节顺序不正确,并且您总是通过函数运行该值以将其设置为网络字节顺序。如果有必要,该函数将进行神奇的转换,这样您的代码就可以移植到不同深度的机器上。

好的。您可以转换两种类型的数字:(两个字节)和(四个字节)。这些函数也适用于无符号的变体。假设您想将主机字节顺序转换为网络字节顺序。从“主机”的“h”开始,接下来是“to”,然后是“网络”的“n”,以及“短”的“s”: h-to-n-s或hton()(阅读:“主机到网络短”)。

这几乎太容易了...

你可以使用你想要的“n”、“h”、“s”和“l”的每一个组合,不算真正愚蠢的组合。例如,没有stolh()(“短到长主机”)函数——至少在这个聚会上没有。但是有:

FunctionDescriptionhtons()host to network shorthtonl()host to network longntohs()network to host shortntohl()network to host long

基本上,您需要在这些数字上线之前将其转换为网络字节顺序,并在它们上线后将其转换为主机字节顺序。

抱歉,我不知道有什么64位变体。如果你想做浮点运算,请查看下面关于序列化的部分。

假设本文档中的数字按主机字节顺序排列,除非我另有说明。

3.3结构

好了,我们终于到这里了。是时候谈论编程了。在这一部分,我将介绍套接字接口使用的各种数据类型,因为其中一些数据类型很难弄清楚。

首先是简单的:套接字描述符。套接字描述符是以下类型:

    int

只是一个普通的int

从这里开始事情变得很奇怪,所以请通读并忍受我。

我的第一个结构™——结构addrinfo。这个结构是最近的发明,用于为后续使用准备套接字地址结构。它也用于主机名查找和服务名查找。当我们以后开始实际使用时,这会更有意义,但是现在要知道,这是建立连接时首先要调用的东西之一。

    struct addrinfo {        int              ai_flags;     // AI_PASSIVE, AI_CANONNAME, etc.        int              ai_family;    // AF_INET, AF_INET6, AF_UNSPEC        int              ai_socktype;  // SOCK_STREAM, SOCK_DGRAM        int              ai_protocol;  // use 0 for "any"        size_t           ai_addrlen;   // size of ai_addr in bytes        struct sockaddr *ai_addr;      // struct sockaddr_in or _in6        char            *ai_canonname; // full canonical hostname            struct addrinfo *ai_next;      // linked list, next node    };

您将加载这个结构,然后调用getaddrinfo()。它将返回一个指针,指向这些结构的新链表,其中填充了您需要的所有好东西。

您可以强制它在ai_family字段中使用IPv4或IPv6,或者让它AF_UNSPEC使用任何东西。这很酷,因为您的代码可以与知识产权版本无关。

请注意,这是一个链表:ai_next下一个元素的点——可能有几个结果供您选择。我会使用第一个有效的结果,但您可能有不同的业务需求;我不是什么都知道,伙计!

您将看到,ai_addr字段中的结构addrinfo是一个指向结构sokaddr的指针。这是我们开始进入IP地址结构中的细节的地方。

您通常可能不需写入这些结构;通常,您只需要调用getaddrinfo()来填写您的结构addrinfo。*然而,*您必须在这些结构内部查看以获取值,所以我在这里展示它们。

(另外,所有的代码都是在struct addrinfo发明之前编写的,我们都是手工打包的,所以你会在野外看到很多IPv4代码就是这样做的。你知道,在这个指南的旧版本中等等。)

有些结构是IPv4,有些是IPv6,有些两者都是。我会记下哪些是什么。

不管怎样,这一结构包含了许多类型套接字的套接字地址信息。

    struct sockaddr {        unsigned short    sa_family;    // address family, AF_xxx        char              sa_data[14];  // 14 bytes of protocol address    }; 

sa_family可以是各种各样的东西,但是对于我们在本文档中所做的一切来说,它都是AF_INET(IPv4)或AF_INET6(IPv6)。sa_data包含套接字的目标地址和端口号。这相当笨拙,因为您不想用手在sa_data中繁琐地打包地址。

为了处理这一结构,程序员创建了一个并行结构:用于IPv4的结构sockaddr_in(Internet的in)。

这是最重要的一点:指向结构sockaddr_in的指针可以被转换为指向结构的指针,反之亦然。因此,即使连接()想要一个结构的sokaddr*,您仍然可以使用结构sockaddr_in并在最后一刻转换它!

    // (IPv4 only--see struct sockaddr_in6 for IPv6)        struct sockaddr_in {        short int          sin_family;  // Address family, AF_INET        unsigned short int sin_port;    // Port number        struct in_addr     sin_addr;    // Internet address        unsigned char      sin_zero[8]; // Same size as struct sockaddr    };

这种结构使得引用套接字地址的元素变得容易。请注意,sin_zero(包括用于将结构填充到一个结构体sokaddr的长度)应该用函数memset()设置为所有0。此外,请注意sin_family对应于结构体sokaddr中的sa_family,并且应该设置为“AF_INET”。最后,sin_port必须是网络字节顺序(通过使用hton()!

让我们深入挖掘!你看sin_addr场是一个结构in_addr。那是什么?嗯,不是太戏剧化,但它是有史以来最可怕的结合之一:

    // (IPv4 only--see struct in6_addr for IPv6)        // Internet address (a structure for historical reasons)    struct in_addr {        uint32_t s_addr; // that's a 32-bit int (4 bytes)    };

哇!它曾经是一个联合,但是现在那些日子似乎已经过去了。很好的解脱。所以如果你已经声明ina是结构sockaddr_in类型,那么ina.sin_addr。s_addr引用4字节的IP地址(以网络字节顺序)。请注意,即使你的系统仍然使用结构in_addr的糟糕的联合,你仍然可以引用4字节的IP地址,就像我上面所做的一样(这是由于#定义)

IPv6呢?它也有类似的结构

    // (IPv6 only--see struct sockaddr_in and struct in_addr for IPv4)        struct sockaddr_in6 {        u_int16_t       sin6_family;   // address family, AF_INET6        u_int16_t       sin6_port;     // port number, Network Byte Order        u_int32_t       sin6_flowinfo; // IPv6 flow information        struct in6_addr sin6_addr;     // IPv6 address        u_int32_t       sin6_scope_id; // Scope ID    };        struct in6_addr {        unsigned char   s6_addr[16];   // IPv6 address    };

请注意,IPv6有一个IPv6地址和一个端口号,就像IPv4有一个IPv4地址和一个端口号一样。

另外请注意,我暂时不会谈论IPv6流信息或范围标识字段...这只是一个入门指南。:-

最后但并非最不重要的是,这里还有一个简单的结构,结构sockaddr_storage,它被设计得足够大,可以容纳IPv4和IPv6结构。对于一些调用,有时你不知道它是否会用IPv4或IPv6地址填充你的结构Sockaddr。所以你传入这个并行结构,除了更大之外,非常类似结构Sockaddr,然后把它转换成你需要的类型:

    struct sockaddr_storage {        sa_family_t  ss_family;     // address family            // all this is padding, implementation specific, ignore it:        char      __ss_pad1[_SS_PAD1SIZE];        int64_t   __ss_align;        char      __ss_pad2[_SS_PAD2SIZE];    };

重要的是,您可以在ss_family字段中看到地址族——检查它是AF_INET还是AF_INET6(对于IPv4或IPv6)。然后,如果您愿意,您可以将其转换为结构sockaddr_in结构sockaddr_in6

3.4 IP地址,第二部分

对你来说幸运是,有一堆函数允许你操作IP地址。不需要手工计算它们,也不需要用<<操作符把它们塞进去。

首先,假设您有一个结构体sockaddr_inina,并且您有一个要存储在其中的IP地址10.12.110.57或2001:db8:63b3:1::3490。您要使用的函数inet_pton()将一个以数字和点表示的IP地址转换为结构体in_addr或结构体in6_addr,具体取决于您指定AF_INET还是AF_INET6。(pton代表“向网络展示”——如果更容易记住,您可以称之为“可打印到网络”。)转换可以进行如下:

    struct sockaddr_in sa; // IPv4    struct sockaddr_in6 sa6; // IPv6        inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4    inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6

(快速注意:旧的做事方式使用了一个名为inet_addr()的函数或另一个名为inet_aton()的函数;这些现在已经过时,不适用于IPv6。)

现在,上面的代码片段不是很健壮,因为没有错误检查。请参见,inet_pton()在错误时返回-1,如果地址混乱则返回0。因此在使用之前请检查以确保结果大于0!

好的,现在您可以将字符串IP地址转换为它们的二进制表示形式。反过来呢?如果您有一个结构in_addr,并且您想用数字和点表示法打印它呢?(或者您想要的结构in6_addr,呃,“十六进制和冒号”表示法。)在这种情况下,您需要使用函数inet_ntop()(“ntop”意味着“网络到呈现”——如果更容易记住,您可以称之为“网络到可打印”),如下所示:

// IPv4:char ip4[INET_ADDRSTRLEN];  // space to hold the IPv4 stringstruct sockaddr_in sa;      // pretend this is loaded with somethinginet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);printf("The IPv4 address is: %s\n", ip4);// IPv6:char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 stringstruct sockaddr_in6 sa6;    // pretend this is loaded with somethinginet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);printf("The address is: %s\n", ip6);

当您调用它时,您将传递地址类型(IPv4或IPv6)、地址、保存结果的字符串指针以及该字符串的最大长度。(两个宏可以方便地保存保存最大IPv4或IPv6地址所需的字符串的大小:INET_ADDRSTRLENINET6_ADDRSTRLEN。)

(另一个快速提示再次提到旧的做事方式:执行此转换的历史函数称为inet_ntoa()。它也过时了,不能与IPv6一起工作。)

最后,这些函数只适用于数字IP地址——它们不会对主机名进行任何命名服务器DNS查找,比如“www.example.com”。您将使用getaddrinfo()来完成这一任务,稍后您将看到。

3.4.1专用(或断开)网络

许多地方都有防火墙,为了保护自己,它将网络隐藏在世界其他地方之外。通常情况下,防火墙使用称为网络地址转换(NAT)的过程将“内部”IP地址转换为“外部”(世界上其他人都知道)IP地址。

你开始紧张了吗?"他带着这些奇怪的东西要去哪里?"

好吧,放松一下,给自己买一杯不含酒精(或酒精)的饮料,因为作为初学者,你甚至不必担心NAT,因为它已经为你透明地完成了。但是我想谈谈防火墙后面的网络,以防你开始被你看到的网络号码弄糊涂。

例如,我家里有一个防火墙。我有两个DSL公司分配给我的静态IPv4地址,但我在网络上有七台电脑。这怎么可能呢?两台电脑不能共享同一个IP地址,否则数据就不知道该去哪一台!

答案是:它们不共享相同的IP地址。它们在一个私有网络上,分配了2400万的IP地址。它们都是为我准备的。就其他人而言,都是为我准备的。事情是这样的:

如果我登录到一台远程计算机,它会告诉我我是从192.0.2.33登录的,这是我的互联网服务提供商提供给我的公共IP地址。但是如果我问我的本地计算机它的IP地址是什么,它会说10.0.0.5。谁在把IP地址从一个翻译到另一个?没错,防火墙!它在做NAT!

10. x. x. x是少数几个只在完全断开的网络或防火墙后的网络上使用的保留网络之一。RFC 191818概述了可供您使用的专用网络号码的详细信息,但您将看到的一些常见号码是10. x. x. x192.168. x. x,其中x通常是0-255。不太常见的是172. y. x. x,其中y介于16和31之间。

NATing防火墙后面的网络不需要在这些保留网络中的一个上,但它们通常是。

(有趣的事实!我的外部IP地址并不192.0.2.33。192.0.2. x网络是为在文档中使用的假想的“真实”IP地址保留的,就像这个指南一样!沃兹!)

从某种意义上说,IPv6也有专用网络。根据RFC 419319,它们将从fdXX:(或者在未来可能是fcXX:)开始。然而,NAT和IPv6通常不会混合(除非你正在做IPv6到IPv4网关的事情,这超出了本文的范围)——理论上,你将有如此多的地址可供支配,以至于你不再需要使用NAT。但是如果你想在一个不会向外路由的网络上为自己分配地址,下面是方法。

4从IPv4跳转到IPv6

但是我只想知道我的代码中要改变什么才能让它与IPv6一起运行!现在就告诉我!

好的!好的!

这里的几乎所有内容都是我在上面已经讨论过的,但它是不耐烦者的简短版本。(当然,不止这些,但这是适用于指南的。)

  1. 首先,尝试使用getaddrinfo()获取所有的结构信息,而不是手工打包结构。这将使您的IP版本不可知,并将消除许多后续步骤。
  2. 任何地方,你发现你硬编码任何有关的IP版本,尝试包装在一个帮助函数。
  3. AF_INET改成AF_INET6
  4. PF_INET改成PF_INET6
  5. INADDR_ANY作业改为in6addr_any作业,略有不同:
    struct sockaddr_in sa;    struct sockaddr_in6 sa6;    sa.sin_addr.s_addr = INADDR_ANY;  // use my IPv4 address    sa6.sin6_addr = in6addr_any; // use my IPv6 address

此外,IN6ADDR_ANY_INIT值可以在声明结构in6_addr时用作初始化器,如下所示:

    struct in6_addr ia6 = IN6ADDR_ANY_INIT;
  1. sockaddr_in使用结构sockaddr_in6,确保在适当的字段中添加“6”(参见上面的结构)。没有sin6_zero字段。
  2. in_addr使用结构in6_addr,确保在适当的字段中添加“6”(参见上面的结构)。
  3. inet_pton代替inet_aton()或inet_addr()
  4. inet_ntop代替inet_ntoa()。
  5. 使用高级getaddrinfo()而不是gethostbyname()。
  6. 使用高级getnameinfo()代替gethostbyaddr()(尽管gethostbyaddr()仍然可以与IPv6一起工作)。
  7. INADDR_BROADCAST不再工作。请改用IPv6多播。

它就在这里

5系统调用或破产

这是我们进入系统调用(和其他库调用)的部分,这些调用允许您访问Unix框或任何支持套接字应用编程接口的框的网络功能(BSD、视窗、Linux、苹果、你有什么)。)当你调用其中一个函数时,内核会自动接管并为你完成所有工作。

大多数人被困在这里的地方是调用这些东西的顺序。在这种情况下,手册页是没有用的,正如你可能已经发现的那样。为了帮助解决这个可怕的情况,我试图在下面的部分中以与你在程序中调用它们完全``*(大约*``)相同的顺序列出系统调用。

再加上到处都是一些示例代码,一些牛奶和饼干(我担心你必须自己提供),以及一些原始的勇气和勇气,你会像乔恩·波斯特之子一样在互联网上传播数据!

(请注意,为了简洁起见,下面的许多代码片段不包括必要的错误检查。它们通常假设调用getaddrinfo()的结果*成功*,并返回链接列表中的有效条目。不过,这两种情况在独立程序中都得到了适当的解决,所以请将它们作为一个模型。)

5.1 getaddrinfo()-准备发射!

这是一个有很多选项的函数的真正主力,但是用法实际上非常简单。它有助于设置您以后需要的结构

一点历史:过去,你会使用一个叫做gethostbyname)的函数来进行DNS查找。然后你会手工将这些信息加载到一个结构sockaddr_in中,并在你的调用中使用它。

谢天谢地,这不再是必要的。(如果你想编写适用于IPv4和IPv6的代码,这也不是可取的!)在当今时代,你现在有了getaddrinfo()函数,它可以为你做各种各样的好事,包括DNS和服务名称查找,并填写你需要的结构,此外!

我们一起来看看吧!

    #include <sys/types.h>    #include <sys/socket.h>    #include <netdb.h>        int getaddrinfo(const char *node,     // e.g. "www.example.com" or IP                    const char *service,  // e.g. "http" or port number                    const struct addrinfo *hints,                    struct addrinfo **res);你给这个函数三个输入参数,它给你一个指向结果链表res的指针。

节点参数是要连接的主机名或IP地址。

接下来是参数服务,它可以是端口号,如80,或者特定服务的名称(在IANA端口列表20或Unix机器上的 /etc/services文件中找到),如超文本传输协议、ftp、telnet或smtp或其他任何名称。

最后,提示参数指向您已经用相关信息填写的结构addrinfo

如果您是一个想要监听主机IP地址端口3490的服务器,下面是一个示例调用。请注意,这实际上并没有进行任何监听或网络设置;它只是设置了我们稍后将使用的结构:

int status;struct addrinfo hints;struct addrinfo *servinfo;  // will point to the resultsmemset(&hints, 0, sizeof hints); // make sure the struct is emptyhints.ai_family = AF_UNSPEC;     // don't care IPv4 or IPv6hints.ai_socktype = SOCK_STREAM; // TCP stream socketshints.ai_flags = AI_PASSIVE;     // fill in my IP for meif ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {    fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));    exit(1);}// servinfo now points to a linked list of 1 or more struct addrinfos// ... do everything until you don't need servinfo anymore ....freeaddrinfo(servinfo); // free the linked-list

请注意,我将ai_family设置为AF_UNSPEC,从而表明我不在乎我们使用的是IPv4还是IPv6。如果您特别想要其中之一,您可以将其设置为AF_INET或AF_INET6

此外,您将看到AI_PASSIVE标志;这告诉getaddrinfo()将本地主机的地址分配给套接字结构。这很好,因为这样您就不必硬编码它。(或者您可以将特定的地址作为getaddrinfo()的第一个参数,我目前在上面有NULL。)

然后我们进行调用。如果有错误(getaddrinfo()返回非零),我们可以使用函数gai_strerror()打印出来,如您所见。但是,如果一切正常,servinfo将指向一个结构地址的链表,每个链表都包含一个我们以后可以使用的结构地址!漂亮!

最后,当我们最终完成getaddrinfo()如此慷慨地为我们分配的链接列表时,我们可以(也应该)通过调用freaddrinfo()来释放它。

如果你是一个想要连接到特定服务器的客户端,这里有一个示例调用,比如“www.example.net”端口3490。同样,这实际上并没有连接,但它设置了我们稍后将使用的结构:

int status;struct addrinfo hints;struct addrinfo *servinfo;  // will point to the resultsmemset(&hints, 0, sizeof hints); // make sure the struct is emptyhints.ai_family = AF_UNSPEC;     // don't care IPv4 or IPv6hints.ai_socktype = SOCK_STREAM; // TCP stream sockets// get ready to connectstatus = getaddrinfo("www.example.net", "3490", &hints, &servinfo);// servinfo now points to a linked list of 1 or more struct addrinfos// etc.

我一直在说servinfo是一个包含各种地址信息的链表。让我们写一个快速演示程序来展示这些信息。这个简短的程序21将打印你在命令行指定的任何主机的IP地址:

/*** showip.c -- show IP addresses for a host given on the command line*/#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <netdb.h>#include <arpa/inet.h>#include <netinet/in.h>int main(int argc, char *argv[]){    struct addrinfo hints, *res, *p;    int status;    char ipstr[INET6_ADDRSTRLEN];    if (argc != 2) {        fprintf(stderr,"usage: showip hostname\n");        return 1;    }    memset(&hints, 0, sizeof hints);    hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version    hints.ai_socktype = SOCK_STREAM;    if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));        return 2;    }    printf("IP addresses for %s:\n\n", argv[1]);    for(p = res;p != NULL; p = p->ai_next) {        void *addr;        char *ipver;        // get the pointer to the address itself,        // different fields in IPv4 and IPv6:        if (p->ai_family == AF_INET) { // IPv4            struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;            addr = &(ipv4->sin_addr);            ipver = "IPv4";        } else { // IPv6            struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;            addr = &(ipv6->sin6_addr);            ipver = "IPv6";        }        // convert the IP to a string and print it:        inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);        printf("  %s: %s\n", ipver, ipstr);    }    freeaddrinfo(res); // free the linked list    return 0;}

正如您所看到的,代码调用getaddrinfo(),无论您在命令行上传递什么,它都会填写res所指向的链表,然后我们可以遍历该列表并打印出来,或者做任何事情。

(这里有一点丑陋,我们必须根据IP版本深入研究不同类型的结构sokaddrs。对此我很抱歉!我不确定有更好的方法。)

样本运行!每个人都喜欢截图:

    $ showip www.example.net    IP addresses for www.example.net:          IPv4: 192.0.2.88        $ showip ipv6.example.com    IP addresses for ipv6.example.com:          IPv4: 192.0.2.101      IPv6: 2001:db8:8c00:22::171

现在我们已经控制住了,我们将使用从getaddrinfo()获得的结果传递给其他套接字函数,最后,建立我们的网络连接!继续阅读!

5.2套接字()-获取文件描述符!

我想我不能再推迟了——我必须谈谈套接字()系统调用。下面是细分:

    #include <sys/types.h>    #include <sys/socket.h>        int socket(int domain, int type, int protocol); 

但是这些参数是什么呢?它们允许您说出您想要什么样的套接字(IPv4或IPv6、流或数据报以及TCP或UDP)。

过去人们会硬编码这些值,你现在绝对可以这样做。(域是PF_INETPF_INET6类型SOCK_STREAMSOCK_DGRAM协议可以设置为0来为给定类型选择合适的协议。或者你可以调用get原型名()来查找你想要的协议,“tcp”或“udp”。)

(这个PF_INET的东西是AF_INET的近亲,您可以在初始化结构sockaddr_in中的sin_family字段时使用它。事实上,它们关系如此密切,以至于它们实际上具有相同的值,许多程序员会调用套接字()并将AF_INET作为第一个参数,而不是PF_INET。现在,得到一些牛奶和饼干,因为是时候讲一个故事了。很久很久以前,人们认为一个地址族(AF_INET中的“AF”代表什么)可能支持他们的协议族(PF_INET中的“PF”代表什么)引用的几个协议。但这并没有发生。从那以后,他们都过着幸福的生活,结束。所以最正确的做法是在你的结构sockaddr_in中使用AF_INET,在你对套接字()的调用中使用PF_INET。)

总之,够了。你真正想做的是使用调用getaddrinfo()的结果中的值,并将它们直接输入套接字(),如下所示:

int s;struct addrinfo hints, *res;// do the lookup// [pretend we already filled out the "hints" struct]getaddrinfo("www.example.com", "http", &hints, &res);// again, you should do error-checking on getaddrinfo(), and walk// the "res" linked list looking for valid entries instead of just// assuming the first one is good (like many of these examples do).// See the section on client/server for real examples.s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

套接字()只返回一个套接字描述符,您可以在以后的系统调用中使用它,或者在出错时返回-1。全局变量errno被设置为错误的值(有关更多详细信息,请参阅errno手册页,以及在多线程程序中使用errno的快速注释)。

好吧,好吧,好吧,但是这个套接字有什么好处呢?答案是它本身真的没有好处,你需要继续阅读并进行更多的系统调用,这样它才有意义。

5.3 bind()-我在哪个端口?

一旦你有了一个套接字,你可能必须将该套接字与本地机器上的端口相关联。(如果你要在特定端口上监听()传入连接,这通常是这样做的——多人网络游戏在告诉你“连接到192.168.5.10端口3490”时会这样做。)端口号被内核用来将传入的数据包与某个进程的套接字描述符相匹配。如果您只做一个连接()(因为您是客户端,而不是服务器),这可能是不必要的。无论如何都要阅读它,只是为了好玩。

下面是bind)系统调用的概要:

    #include <sys/types.h>    #include <sys/socket.h>        int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

Sockfd套接字()返回的套接字文件描述符。my_addr是指向包含您的地址(即端口和IP地址)信息的结构Sockaddr的指针。addrlen是该地址的字节长度。

咻。这在一个块中有点难吸收。让我们举一个例子,将套接字绑定到程序运行的主机,端口3490:

struct addrinfo hints, *res;int sockfd;// first, load up address structs with getaddrinfo():memset(&hints, 0, sizeof hints);hints.ai_family = AF_UNSPEC;  // use IPv4 or IPv6, whicheverhints.ai_socktype = SOCK_STREAM;hints.ai_flags = AI_PASSIVE;     // fill in my IP for megetaddrinfo(NULL, "3490", &hints, &res);// make a socket:sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);// bind it to the port we passed in to getaddrinfo():bind(sockfd, res->ai_addr, res->ai_addrlen);

通过使用AI_PASSIVE标志,我告诉程序绑定到它运行的主机的IP。如果您想绑定到特定的本地IP地址,请删除AI_PASSIVE,并在getaddrinfo()的第一个参数中放入一个IP地址。

bind)在错误时也返回-1,并将errno设置为错误的值。

许多旧代码在调用bind()之前手动打包sockaddr_in的结构。显然这是IPv4特有的,但是没有什么能阻止你对IPv6做同样的事情,除了使用getaddrinfo()通常会更容易。不管怎样,旧代码看起来像这样:

// !!! THIS IS THE OLD WAY !!!int sockfd;struct sockaddr_in my_addr;sockfd = socket(PF_INET, SOCK_STREAM, 0);my_addr.sin_family = AF_INET;my_addr.sin_port = htons(MYPORT);     // short, network byte ordermy_addr.sin_addr.s_addr = inet_addr("10.12.110.57");memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);

在上面的代码中,如果您想绑定到本地IP地址(如上面的AI_PASSIVE标志),您还可以将INADDR_ANY分配给s_addr字段。IPv6版本的INADDR_ANY是一个全局变量in6addr_any,分配到您的结构sockaddr_in6的sin6_addr字段中。(还有一个宏IN6ADDR_ANY_INIT,您可以在变量初始化器中使用。)

调用bind()时需要注意的另一件事是:不要使用端口号。所有低于1024的端口都是保留的(除非你是超级用户)!你可以有任何高于1024的端口号,直到65535(前提是它们还没有被其他程序使用)。

有时,您可能会注意到,您试图重新运行服务器,但bind()失败,声称“地址已在使用中”。这是什么意思?嗯,一点连接的套接字仍然挂在内核中,它占用了端口。您可以等待它清除(大约一分钟),或者向您的程序添加代码,允许它重用端口,如下所示:

int yes=1;//char yes='1'; // Solaris people use this// lose the pesky "Address already in use" error messageif (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof yes) == -1) {    perror("setsockopt");    exit(1);} 

关于bind()的最后一个小注意事项:有时您不一定要调用它。如果您正在连接()到远程机器,并且您不关心您的本地端口是什么(就像telnet的情况一样,您只关心远程端口),您可以简单地调用连接(),它将检查套接字是否未绑定,并在必要时将它绑定到未使用的本地端口。

5.4连接()-嘿,你!

让我们假装几分钟你是一个telnet应用程序。你的用户命令你(就像电影TRON中一样*)*获取一个套接字文件描述符。你遵从并调用套接字()。接下来,用户告诉你连接到端口“23”(标准telnet端口)上的“10.12.110.57”。哟!你现在怎么办?

你很幸运,程序,你现在正在细读关于连接()的部分——如何连接到远程主机。所以请继续阅读!没时间浪费了!

连接()调用如下:

    #include <sys/types.h>    #include <sys/socket.h>        int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); 

Sockfd是我们友好的邻域套接字文件描述符,由套接字()调用返回,serv_addr是包含目标端口和IP地址的结构Sockaddr,addrlen是服务器地址结构的长度(以字节为单位)。

所有这些信息都可以从getaddrinfo()调用的结果中收集到,该调用非常有用。

这开始更有意义了吗?我在这里听不到你,所以我只能希望是这样。让我们举一个例子,我们在这里建立一个套接字连接到端口3490“www.example.com”

struct addrinfo hints, *res;int sockfd;// first, load up address structs with getaddrinfo():memset(&hints, 0, sizeof hints);hints.ai_family = AF_UNSPEC;hints.ai_socktype = SOCK_STREAM;getaddrinfo("www.example.com", "3490", &hints, &res);// make a socket:sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);// connect!connect(sockfd, res->ai_addr, res->ai_addrlen);

同样,老式程序填写了自己的结构sockaddr_ins传递给连接()。如果你愿意,你可以这样做。参见上面bind部分的类似注释。

一定要检查连接()的返回值-它会返回-1的错误,并设置变量errno

另外,请注意我们没有调用bind()。基本上,我们不关心我们的本地端口号;我们只关心我们要去哪里(远程端口)。内核会为我们选择一个本地端口,我们连接的站点会自动从我们这里获取这些信息。不用担心。

5.5听()-有人能打电话给我吗?

好的,是时候改变一下节奏了。如果你不想连接到远程主机怎么办。比方说,只是为了好玩,你想等待传入的连接,并以某种方式处理它们。这个过程有两个步骤:首先你听(),然后你接受()(见下文)。

听()调用相当简单,但需要一点解释:

    int listen(int sockfd, int backlog)

ockfd套接字()系统调用中常用的套接字文件描述符。积压是传入队列中允许的连接数。这是什么意思?传入的连接将在这个队列中等待,直到您接受()它们(见下文),这是可以排队的数量限制。大多数系统默默地将这个数字限制在大约20个;您可能可以将它设置为510而不受影响。

同样,像往常一样,听()返回-1,并设置errno错误。

正如你可能想象的那样,我们需要先调用bind(),然后再调用List(),这样服务器就可以在特定的端口上运行。(你必须能够告诉你的朋友要连接到哪个端口!所以如果你要监听传入的连接,你将进行的系统调用的顺序是:

getaddrinfo();socket();bind();listen();/* accept() goes here */ 

我将把它放在示例代码中,因为它是相当不言自明的。(下面接受()部分的代码更完整。)整个sha-ang中真正棘手的部分是调用接受()。

5.6接受()-"感谢您拨打端口3490。"

准备好——接受()调用有点奇怪!会发生这样的事情:远处的某个人会试图在你正在监听()的端口上连接()到你的机器。他们的连接会排队等待接受()。你调用接受(),并告诉它获取挂起的连接。它会返回一个全新的套接字文件描述符给你,用于这个单一的连接!没错,突然你有了两个套接字文件描述符,一个的价格!原来的那个还在监听更多的新连接,新创建的那个终于准备好发送()和recv)了。我们到了!

电话如下:

    #include <sys/types.h>    #include <sys/socket.h>        int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

Sockfd是听()ing套接字描述符。很简单。addr通常是指向本地结构sockaddr_storage的指针。这是关于传入连接的信息的去处(通过它,您可以确定哪个主机从哪个端口调用您)。addrlen是一个本地整数变量,在其地址传递给接受()之前,应该设置为sizeof(结构sockaddr_storage)。接受()不会在addr中放入超过那么多字节。如果它放入的少,它会改变addrlen的值来反映这一点。

你猜怎么着?接受()返回-1如果发生错误,设置errno。Betcha没有想到这一点。

像以前一样,这是一个可以在一个块中吸收的集合,所以这里有一个示例代码片段供您阅读:

#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <netdb.h>#define MYPORT "3490"  // the port users will be connecting to#define BACKLOG 10     // how many pending connections queue will holdint main(void){    struct sockaddr_storage their_addr;    socklen_t addr_size;    struct addrinfo hints, *res;    int sockfd, new_fd;    // !! don't forget your error checking for these calls !!    // first, load up address structs with getaddrinfo():    memset(&hints, 0, sizeof hints);    hints.ai_family = AF_UNSPEC;  // use IPv4 or IPv6, whichever    hints.ai_socktype = SOCK_STREAM;    hints.ai_flags = AI_PASSIVE;     // fill in my IP for me    getaddrinfo(NULL, MYPORT, &hints, &res);    // make a socket, bind it, and listen on it:    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);    bind(sockfd, res->ai_addr, res->ai_addrlen);    listen(sockfd, BACKLOG);    // now accept an incoming connection:    addr_size = sizeof their_addr;    new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);    // ready to communicate on socket descriptor new_fd!    .    .    .

同样,请注意,我们将使用套接字描述符new_fd所有的发送()和recv()调用。如果您只得到一个连接,如果您愿意,您可以关闭()侦听ocockfd,以防止同一端口上有更多的传入连接。

5.7发送()和回复()-跟我说话,宝贝!

这两个函数用于通过流套接字或连接的数据报套接字进行通信。如果您想使用常规的未连接数据报套接字,您需要看到下面关于sendto()和recvfrom()的部分。

发送()调用:

    int send(int sockfd, const void *msg, int len, int flags)

Sockfd是您要发送数据到的套接字描述符(无论它是通过套接字()返回的还是通过接受()获得的)。msg是您要发送的数据的指针,len是该数据的长度,以字节为单位。只需将标志设置0。(有关标志的详细信息,请参阅发送()手册页。)

一些示例代码可能是:

char *msg = "Beej was here!";int len, bytes_sent;...len = strlen(msg);bytes_sent = send(sockfd, msg, len, 0);... 

发送()返回实际发送的字节数——这可能比你告诉它发送的字节数要少!看,有时候你告诉它发送一大堆数据*,它就是*无法处理。它会尽可能多地发送数据,并相信你稍后会发送其余的数据。记住,如果*发送()*返回的值与len中的值不匹配,就由你来发送字符串的其余部分。好消息是:如果数据包很小(小于1K左右),它可能会一次性发送所有内容。同样,-1在错误时返回,errno被设置为错误号。

recv)调用在许多方面类似:

    int recv(int sockfd, void *buf, int len, int flags);

Sockfd是要读取的套接字描述符,buf是要读取信息的缓冲区,len是缓冲区的最大长度,标志可以再次设置为0。(有关标志信息,请参阅recv()手册页。)

recv()返回实际读入缓冲区的字节数,或-1(相应地设置errno)。

等待!recv()可以返回0。这只意味着一件事:远程端已经关闭了您的连接!返回值为0recv()让您知道发生了这种情况的方式。

这很简单,不是吗?你现在可以在流套接字上来回传递数据了!哇!你是一个Unix网络程序员!

5.8 sendto()和recvfrom()-跟我说话,DGRAM风格

“这一切都很好,”我听到你说,“但是这给我留下了没有连接的数据报套接字吗?”没问题,朋友。我们正好有东西。

由于数据报套接字没有连接到远程主机,猜猜我们在发送数据包之前需要给出哪条信息?没错。目的地地址!下面是独家新闻:

    int sendto(int sockfd, const void *msg, int len, unsigned int flags,               const struct sockaddr *to, socklen_t tolen); 正如你所看到的,这个调用基本上是相同的调用发送()添加了另外两条信息。to是一个指针,指向一个结构sokaddr(这可能是另一个结构sockaddr_in或结构sockaddr_in6或结构sockaddr_storage,你在最后一分钟投),其中包含目标IP地址和端口。tolen,一个int深层,可以简单地设置为sizeof*to或sizeof(结构sockaddr_storage)。

要获得目标地址结构,您可能要么从getaddrinfo()或下面的recvfrom()获得,要么手工填写。

就像在发送()中一样,sendto()返回实际发送的字节数(同样,可能少于您告诉它发送的字节数!),或者在错误时返回-1

recv()和recvfrom()同样相似。recvfrom()的概要是:

    int recvfrom(int sockfd, void *buf, int len, unsigned int flags,                 struct sockaddr *from, int *fromlen); 同样,这就像recv()添加了几个字段。from是一个指向本地结构sockaddr_storage的指针,该结构将填充原始机器的IP地址和端口。Fromlen是一个指向本地int的指针,该int应该初始化为sizeof*from或sizeof(结构sockaddr_storage)。当函数返回时,Fromlen将包含实际存储在from中的地址的长度。

recvfrom()返回接收到的字节数,或-1(相应地设置errno)。

所以,这里有一个问题:为什么我们使用结构sockaddr_storage作为套接字类型?为什么不结构sockaddr_in?因为,你看,我们不想把自己束缚在IPv4或IPv6上。所以我们使用通用的结构sockaddr_storage我们知道它足够大。

(所以...这里还有另一个问题:为什么结构Sockaddr本身对任何地址来说都不够大?我们甚至将通用结构sockaddr_storage转换为通用结构Sockaddr!看起来是多余的。答案是,它不够大,我想在这一点上改变它会有问题。所以他们做了一个新的。)

请记住,如果您连接了一个数据报套接字(),那么您可以简单地对所有事务使用发送()和recv()。套接字本身仍然是一个数据报套接字,数据包仍然使用UDP,但是套接字接口会自动为您添加目的地和源信息。

5.9关闭()和关闭()-滚出我的脸!

咻!你一整天都在发送()ingrecv()ing数据,你已经受够了。你已经准备好关闭套接字描述符上的连接了。这很容易。你可以只使用常规的Unix文件描述符关闭()函数:

    close(sockfd); 

这将阻止对套接字的任何读写。任何试图在远程端读写套接字的人都会收到错误。

如果您想对套接字如何关闭有更多的控制,您可以使用关闭(函数。它允许您切断某个方向的通信,或者双向的通信(就像关闭()一样)。简介:

    int shutdown(int sockfd, int how)

Sockfd是您要关闭的套接字文件描述符,以及如何关闭以下内容之一:

howEffect0Further receives are disallowed1Further sends are disallowed2Further sends and receives are disallowed (like close())

Shutdown()在成功时返回0,在错误时返回-1(相应地设置errno)。

如果您屈尊在未连接的数据报套接字上使用关闭(),它只会使套接字无法用于进一步的发送()和recv()调用(请记住,如果您连接()您的数据报套接字,您可以使用这些)。

需要注意的是,Shutdown()实际上并没有关闭文件描述符--它只是改变了文件描述符的可用性。要释放套接字描述符,您需要使用关闭()

没什么可说的。

(除了要记住,如果你使用的是Windows和Winsock,你应该调用闭包()而不是关闭()。

5.10 getpeername()-你是谁?

这个功能太简单了。

太简单了,我几乎没有给它自己的部分。但不管怎样,它在这里。

函数getpeername()将告诉您谁在连接的流套接字的另一端。概要:

    #include <sys/socket.h>        int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); 

Sockfd是连接的流套接字的描述符,addr是一个指向结构Sockaddr(或结构sockaddr_in)的指针,它将保存关于连接另一侧的信息,addrlen是一个指向int的指针,它应该初始化为sizeof*addr或sizeof结构Sockaddr)。

函数在错误时返回-1,并相应地设置errno

一旦您有了他们的地址,您可以使用inet_ntop()、getnameinfo()或gethostbyaddr()来打印或获取更多信息。不,您无法获得他们的登录名。(好的,好的。如果另一台计算机正在运行一个ident守护进程,这是可能的。但是,这超出了本文档的范围。查看RFC 141322了解更多信息。)

5.11地名()-我是谁?

比getpeername()更简单的是函数gethostname()。它返回运行程序的计算机的名称。下面的gethostbyname()可以使用该名称来确定本地计算机的IP地址。

还有什么比这更有趣的呢?我可以想到一些事情,但是它们与套接字编程无关。不管怎样,下面是分类:

    #include <unistd.h>        int gethostname(char *hostname, size_t size); 

参数很简单:host name是指向在函数返回时包含主机名的字符数组的指针,size主机名数组的字节长度。

该函数在成功完成时返回0,在错误时返回-1,并像往常一样设置errno

阿酷尔工作室

2021/10/16  阅读:31  主题:默认主题

作者介绍

阿酷尔工作室

恒生研究院