· > 美好的一天结束于听一首歌睡觉

Sockaddr_in和Sockaddr的区别 - Socket套接字编程 - C语言网 (dotcpp.com)

Linux下的socket演示程序 (biancheng.net)

基于TCP的Qt网络通信 | 爱编程的大丙 (subingwen.cn)

以上为借鉴对象

卡哇伊的图片

709c58c45c30513c9db8bff703a492c

Socket

socket是什么?套接字是什么?

网络编程就是编写程序使两台联网的计算机相互交换数据。那么,这两台计算机之间用什么传输数据呢?首先需要物理连接。如今大部分计算机都已经连接到互联网,因此不用担心这一点。

在此基础上,只需要考虑如何编写数据传输程序。但实际上这点也不用愁,因为操作系统已经提供了 socket。即使对网络数据传输的原理不太熟悉,我们也能通过 socket 来编程。

什么是 socket?

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。

socket 的典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。

学习 socket,也就是学习计算机之间如何通信,并编写出实用的程序。

UNIX/Linux 中的 socket 是什么?

在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。

为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:

  • 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
  • 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。

请注意,网络连接也是一个文件,它也有文件描述符!你必须理解这句话。

我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

  • 用 read() 读取从远程计算机传来的数据;
  • 用 write() 向远程计算机写入数据。

你看,只要用 socket() 创建了连接,剩下的就是文件操作了.

Window 系统中的 socket 是什么?

Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。因此,本教程如果涉及 Windows 平台将使用“句柄”,如果涉及 Linux 平台则使用“描述符”。

与 UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专门针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了。

套接字有哪些类型?socket有哪些类型?

这个世界上有很多种套接字socket,比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix套接字)、CCITT X.25地址(X.25 套接字)等。但本教程只讲第一种套接字——Internet 套接字,它是最具代表性的,也是最经典最常用的。以后我们提及套接字,指的都是 Internet 套接字。

根据数据的传输方式,可以将 Internet 套接字分成两种类型。通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。

Internet 套接字其实还有很多其它数据传输方式。

流格式套接字(SOCK_STREAM)

流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。

SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

流格式套接字有自己的纠错机制,在此我们就不讨论了。

SOCK_STREAM 有以下几个特征:

  • 数据在传输过程中不会消失;
  • 数据是按照顺序传输的;
  • 数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。

可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。

为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。

你也许见过 TCP,是因为你经常听说“TCP/IP”。TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。

那么,“数据的发送和接收不同步”该如何理解呢?

假设传送带传送的是水果,接收者需要凑齐 100 个后才能装袋,但是传送带可能把这 100 个水果分批传送,比如第一批传送 20 个,第二批传送 50 个,第三批传送 30 个。接收者不需要和传送带保持同步,只要根据自己的节奏来装袋即可,不用管传送带传送了几批,也不用每到一批就装袋一次,可以等到凑够了 100 个水果再装袋。

流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。

也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。

流格式套接字有什么实际的应用场景吗?浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析。

数据报格式套接字(SOCK_DGRAM)

数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。

计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。

因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。

可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:

  • 强调快速传输而非传输顺序;
  • 传输的数据可能丢失也可能损毁;
  • 限制每次传输的数据大小;
  • 数据的发送和接收是同步的(有的教程也称“存在数据边界”)。

众所周知,速度是快递行业的生命。用摩托车发往同一地点的两件包裹无需保证顺序,只要以最快的速度交给客户就行。这种方式存在损坏或丢失的风险,而且包裹大小有一定限制。因此,想要传递大量包裹,就得分配发送。

另外,用两辆摩托车分别发送两件包裹,那么接收者也需要分两次接收,所以“数据的发送和接收是同步的”;换句话说,接收次数应该和发送次数相同。

总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。

数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。

QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。

注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。

面向连接和面向无连接套接字

之前我们提到了流式套接字数据报式套接字,在介绍中,我们说到了流式套接字是面向连接的套接字,数据报套接字是面向无连接的套接字。那么面向连接的和面向无连接的套接字有什么区别呢?

举个例子简单地说明面向连接就像有一条管道连接着发送端和接收端,数据包都通过这条管道来传输。当然,两台计算机在通信之前必须先搭建好管道。

无连接就像无头苍蝇乱飞,数据包从发送端到接收端并没有固定的线路,想怎么走就怎么走,只要能到达就行。每个数据包都比较自私,不和别人分享自己的线路,但是,大家最终都能到达接收端。

面向连接和面向无连接套接字

上图是一个简化互联网模型,H1 ~ H6 表示计算机,A~E 表示路由器,发送端发送的数据必须经过路由器的转发才能到达接收端。

假设 H1 要发送若干个数据包给 H6,那么有多条路径可以选择,比如:

路径①:H1 --> A --> C --> E --> H6

路径②:H1 --> A --> B --> E --> H

路径③:H1 --> A --> B --> D --> E --> H6

路径④:H1 --> A --> B --> C --> E --> H6

路径⑤:H1 --> A --> C --> B --> D --> E --> H6

面向连接套接字

面向连接的套接字在正式通信之前要先确定一条路径,没有特殊情况的话,以后就固定使用这条路径来传递数据包了。当然,路径被破坏的话,比如某个路由器断电了,那么会重新建立路径。

很多网络通信教程中,这条预先建立好的路径被称为“虚电路”,就是一条虚拟的通信电路。

为了保证数据包准确、顺序地到达,发送端在发送数据包以后,必须得到接收端的确认才能发送下一个数据包;如果数据包已经发出去了,在数据包发出去的一段时间后仍没有接收端的回应,那么发送端就会重新再发送一次,直到得到接收端的回应。这样就能保证,发送端发送的所有数据包都能准确有序地到达接收端,那么发送端如何得到接收端的回应呢?这就像两个小人在互相发消息的过程,发送端小人为每一个数据包分配ID,然后发送给接收端小人,接收端小人在接收到数据包后就会给发送端小人也回复一个数据包,并且回复发送端:我接收到了一个ID是XX的数据包,就导致了面向连接的套接字会比无连接的套接字多出很多数据包。

面向无连接套接字

面向无连接的套接字,每个数据包都可以选择不同的路径,比如第一个数据包选择路径②,第二个数据包选择路径④,第三个数据包选择路径①……当然,它们也可以选择相同的路径,但那不过是拐一年摇一年的感觉,缘分呐。

每个数据包之间都是一个独立的个体,它们都有着我命由我不由天的气势,它们不服管啊,它们非要各走各路,谁也不care谁,但是它们除了迷路的和发生意外的最后都能到达 H6。可到达的顺序是不确定的,比如:

第一个数据包选择了一条比较长的路径(比如路径⑤),第三个数据包选择了一条比较短的路径(比如路径①),虽然第一个数据包很早就出发了,但是走的路比较远,最终还是晚于第三个数据包达到。

还有一些意外情况的发生,比如:

第一个数据包选择了路径①,但是路由器C突然断电了,那它就到不了 H6 了。第三个数据包选择了路径②,虽然路不远,但是太拥堵,以至于它等待的时间太长,路由器把它丢弃了。

总之,对于无连接的套接字,数据包在传输过程中会发生各种不测,也会发生各种奇迹。H1 只负责把数据包发出,至于它什么时候到达,先到达还是后到达,有没有成功到达,H1 都不管了;H6 也没有选择的权利,只能被动接收,收到什么算什么,爱用不用。

无连接套接字遵循的是尽最大努力完成任务的原则,做不到了也没办法。谁让无连接套接字提供的就是没有质量保证的服务呢。人家讲究的就是快而广比如广播。

Socket编程中的相关概念

1、流(Stream)

计算机中的流其实是一种信息的转换。于某一对象,通常把对象接收外界的信息输入称为输入流,把对象向外输出信息为输出流,合称为输入/输出流(I/O Stream)。

​ 对象间进行数据交换时总是先将数据转换为某种形式的流,再通过流的传输,到达目的对象后再将流转换为对象数据。所以,可以把流看作是一种数据的载体,通过它可以实现数据交换和传输。

2、连接(Connection)

3、阻塞(Block)、非阻塞(Non-block)

阻塞调用是指调用结果返回(或者收到通知)之前,当前线程会被挂起,即不继续执行后续操作;

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

4、同步(Synchronous)、异步(asynchronous)

​ 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。

​ 异步呢,就是发出一个功能调用后,不管没有结果的返回,都不影响当前任务的继续执行。即两个生产线相互独立。

以上两个问题,阻塞非阻塞和同步异步,其实非常相似,但主要区别在于角度不同,同步异步,是对于被调用者而言的;而阻塞非阻塞,则是对调用者而言的。

5、 IP地址 (IP Address) 四段0~255之间的十进制数字,如常见的内网地址192.168.0.1或某公网地址114.55.92.xxx

6、字节顺序(Bytes Order)

​ 字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有小端、大端两种字节顺序。

7、带外数据(Outband Data)

​ 带外数据,也称为TCP紧急数据,是相连的每一对流套接口间一个逻辑上独立的传输通道。带外数据是独立于普通数据传送给用户的。

OSI网络七层模型简明教程

OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

OSI 只是存在于概念和理论上的一种模型,它的缺点是分层太多,增加了网络工作的复杂性,所以没有大规模应用。后来人们对 OSI 进行了简化,合并了一些层,最终只保留了 4 层,从下到上分别是接口层、网络层、传输层和应用层,这就是大名鼎鼎的 TCP/IP 模型。

OSI 七层网络模型和 TCP/IP 四层网络模型的对比

这个网络模型究竟是干什么呢?简而言之就是进行数据封装的。

我们平常使用的程序(或者说软件)一般都是通过应用层来访问网络的,程序产生的数据会一层一层地往下传输,直到最后的网络接口层,就通过网线发送到互联网上去了。数据每往下走一层,就会被这一层的协议增加一层包装,等到发送到互联网上时,已经比原始数据多了四层包装。整个数据封装的过程就像俄罗斯套娃。

当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。

给数据加包装的过程,实际上就是在数据的头部增加一个标志(一个数据块),表示数据经过了这一层,我已经处理过了。给数据拆包装的过程正好相反,就是去掉数据头部的标志,让它逐渐现出原形。

你看,在互联网上传输一份数据是多么地复杂啊,而我们却感受不到,这就是网络模型的厉害之处。我们只需要在代码中调用一个函数,就能让下面的所有网络层为我们工作。

我们所说的 socket 编程,是站在传输层的基础上,所以可以使用 TCP/UDP 协议,但是不能干「访问网页」这样的事情,因为访问网页所需要的 http 协议位于应用层。

两台计算机进行通信时,必须遵守以下原则:

  • 必须是同一层次进行通信,比如,A 计算机的应用层和 B 计算机的传输层就不能通信,因为它们不在一个层次,数据的拆包会遇到问题。
  • 每一层的功能都必须相同,也就是拥有完全相同的网络模型。如果网络模型都不同,那不就乱套了,谁都不认识谁。
  • 数据只能逐层传输,不能跃层。
  • 每一层可以使用下层提供的服务,并向上层提供服务。

TCP/IP协议族

目前实际使用的网络模型是 TCP/IP 模型,它对 OSI 模型进行了简化,只包含了四层,从上到下分别是应用层、传输层、网络层和链路层(网络接口层),每一层都包含了若干协议。

协议(Protocol)就是网络通信过程中的约定或者合同,通信的双方必须都遵守才能正常收发数据。协议有很多种,例如 TCP、UDP、IP 等,通信的双方必须使用同一协议才能通信。协议是一种规范,由计算机组织制定,规定了很多细节,例如,如何建立连接,如何相互识别等。

协议仅仅是一种规范,必须由计算机软件来实现。例如 IP 协议规定了如何找到目标计算机,那么各个开发商在开发自己的软件时就必须遵守该协议,不能另起炉灶。

TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,其中 TCP 和 IP 是最常用的两种底层协议,所以把它们统称为“TCP/IP 协议族”。

也就是说,“TCP/IP模型”中所涉及到的协议称为“TCP/IP协议族”,你可以区分这两个概念,也可以认为它们是等价的,随便你怎么想。

当前学习的socket 编程是基于 TCP 和 UDP 协议的,它们的层级关系如下图所示:

TCP/IP协议的层次图

【扩展阅读】开放式系统(Open System)

把协议分成多个层次有哪些优点?协议设计更容易?当然这也足以成为优点之一。但是还有更重要的原因,就是为了通过标准化操作设计成开放式系统。

标准本身就是对外公开的,会引导更多的人遵守规范。以多个标准为依据设计的系统称为开放式系统(Open System),我们现在学习的 TCP/IP 协议族也属于其中之一。

接下来了解一下开放式系统具有哪些优点。

路由器用来完成 IP 层的交互任务。某个网络原来使用 A 公司的路由器,现要将其替换成 B 公司的,是否可行?这并非难事,并不一定要换成同一公司的同一型号路由器,因为所有生产商都会按照 IP 层标准制造。

再举个例子。大家的计算机是否装有网络接口卡,也就是所谓的网卡?尚未安装也无妨,其实很容易买到,因为所有网卡制造商都会遵守链路层的协议标准。这就是开放式系统的优点。

标准的存在意味着高速的技术发展,这也是开放式系统设计最大的原因所在。实际上,软件工程中的“面向对象(Object Oriented)”的诞生背景中也有标准化的影子。也就是说,标准对于技术发展起着举足轻重的作用。

IP、MAC和端口号——网络通信中确认身份信息的三要素

在茫茫的互联网海洋中,要找到一台计算机非常不容易,有三个要素必须具备,它们分别是 IP 地址、MAC 地址和端口号。

IP地址

IP地址是 Internet Protocol Address 的缩写,译为“网际协议地址”。

目前大部分软件使用 IPv4 地址,但 IPv6 也正在被人们接受,尤其是在教育网中,已经大量使用。

一台计算机可以拥有一个独立的 IP 地址,一个局域网也可以拥有一个独立的 IP 地址(对外就好像只有一台计算机)。对于目前广泛使用 IPv4 地址,它的资源是非常有限的,一台计算机一个 IP 地址是不现实的,往往是一个局域网才拥有一个 IP 地址。

在因特网上进行通信时,必须要知道对方的 IP 地址。实际上数据包中已经附带了 IP 地址,把数据包发送给路由器以后,路由器会根据 IP 地址找到对方的地里位置,完成一次数据的传递。路由器有非常高效和智能的算法,很快就会找到目标计算机。

MAC地址

现实的情况是,一个局域网往往才能拥有一个独立的 IP;换句话说,IP 地址只能定位到一个局域网,无法定位到具体的一台计算机。这可怎么办呀?这样也没法通信啊。

其实,真正能唯一标识一台计算机的是 MAC 地址,每个网卡的 MAC 地址在全世界都是独一无二的。计算机出厂时,MAC 地址已经被写死到网卡里面了(当然通过某些“奇巧淫技”也是可以修改的)。局域网中的路由器/交换机会记录每台计算机的 MAC 地址。

MAC 地址是 Media Access Control Address 的缩写,直译为“媒体访问控制地址”,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address)。

数据包中除了会附带对方的 IP 地址,还会附带对方的 MAC 地址,当数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。

端口号

有了 IP 地址和 MAC 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如 Web 服务(网站)、FTP 服务(文件传输服务)、SMTP 服务(邮箱服务)等,仅有 IP 地址和 MAC 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。如下图所示:

img

TCP的服务端代码流程简述

Socket通信服务端

通过上图我们可以看到一个完整的Socket网络通信,是有客户端和服务端两部分代码组成的,即两个程序(你发给我,我接收;我发给你,你接收)组成。左侧为客户端,右侧为服务端。每一步都是由Socket为我们封装好的函数实现,简单说,我们只需要弄明白每一步的作用和使用方法即可。本章我们先着重讲解服务端的每一步,TCP服务端调用的函数依次是socket( )、bind( )、listen( )、accept( )、recv( )、send( )、closesocket( ),我们会逐一详细的介绍每一个函数的作用及用法。除此之外,还会为大家介绍一下服务端和客户端都会用到的WSAStartup( )函数,在windows系统中我们需要这个函数来以指明 WinSock 规范的版本。以及WSACleanup函数,这个函数用来终止对Socket字库的使用。

Socket编程之WSAStartup函数

上一节我们介绍了客户端与服务端相互通信的模型图,每一步都有具体的函数实现,但需要明白的是,使用这些函数之前,在Windows系统下,需要先调用WSAStartup函数进行必要的初始化,才可以顺利的进行,

而使用WSAStartup函数又需要事先先包含对应的头文件winsock2.h及静态库ws2_32.lib文件(在windows环境下)

具体如下:

函数功能:

用于初始化Socket编程,指明Windows系统中Socket( )版本

依赖静态库库:

ws2_32.lib

需要用#pragma命令包含,即:

1
#pragma comment(lib,"ws2_32.lib")  //表示链接Ws2_32.lib这个库。ws2_32.lib是Winsock2的库文件。

函数原型:

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

返回值类型:

成功返回0,失败返回-1

参数说明:

第一个参数wVersionRequested为 WinSock 规范的版本号它的类型WORD,所以要用MAKEWORD( )宏函数对它的版本号进行转换。低字节为主版本号,高字节为副版本号。

代码示例:

1
wVersionRequired=MAKEWORD(1,2)   //即主版本号是1,副版本号位2,那么它表示的就是调用WinSock 1.2版本。     现在我们一般用2.2版本 ,即MAKEWORD(1,2)

第二个参数lpWSAData 为指向 WSAData 结构体的指针。其定义原型如下:

1
typedef` `struct` `WSAData {``  ``WORD`    `wVersion;  ``//ws2_32.dll 建议我们使用的版本号``  ``WORD`    `wHighVersion;  ``//ws2_32.dll 支持的最高版本号``#ifdef _WIN64``  ``unsigned ``short` `iMaxSockets;   ``//2.0以后不再使用``  ``unsigned ``short` `iMaxUdpDg;   ``//2.0以后不再使用``  ``char`    `*lpVendorInfo;   ``//2.0以后不再使用``  ``char`    `szDescription[WSADESCRIPTION_LEN+1]; ``//一个以 null 结尾的字符串,用来说明 ws2_32.dll 的实现以及厂商信息``  ``char`    `szSystemStatus[WSASYS_STATUS_LEN+1]; ``//一个以 null 结尾的字符串,用来说明 ws2_32.dll 的状态以及配置信息``#else``  ``char`    `szDescription[WSADESCRIPTION_LEN+1];  ``//32位版本,同上``  ``char`    `szSystemStatus[WSASYS_STATUS_LEN+1];  ``//32位版本,同上``  ``unsigned ``short` `iMaxSockets;  ``//32位版本,同上``  ``unsigned ``short` `iMaxUdpDg;  ``//32位版本,同上``  ``char`    `*lpVendorInfo;  32位版本,同上``#endif``} WSADATA, *LPWSADATA;

因此我们只需要包含头文件、静态库,然后完善这两个参数,传入WSAStartup调用即可。

当然,完整可编译的代码,则需要将以上代码放入main函数中,并且包含对应的头文件及相对应的静态库,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//www.dotcpp.com
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")

int main()
{
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData); //目前建议使用最新2.2版本

//以下为测试信息,打印相应的数值用于测试
printf("wVersion: %d.%d\n", LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion));
printf("wHighVersion: %d.%d\n", LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
printf("szDescription: %s\n", wsaData.szDescription);
printf("szSystemStatus: %s\n", wsaData.szSystemStatus);

return 0;
}

socket函数

如下图所示,是Socket通信的原理图,左侧为服务端,右侧是客户端,可以看到服务端的步骤要多一些,客户端将在后面讲解。本节开始将从左侧服务端第一步开始逐步讲解,本步骤目标为创建一个套接字,其返回值为后面的步骤使用。

Socket通信服务端

函数功能:

创建套接字

头文件:

#include <winsock2.h>

函数原型:

int socket( int af, int type, int protocol);

返回值类型:

整型

返回值:

成功返回非负值,表示套接字的文件描述符,失败返回-1,通常返回-1错误很可能是没有执行 WSAStartup初始化导致!

参数说明:

第一个参数af指明了协议族,通常用AF_INET、AF_INET6、AF_LOCAL等。AF表示地址族,选择 AF_INET 的目的就是使用 IPv4 进行通信。因为 IPv4 使用 32 位地址,相比 IPv6 的 128 位来说,计算更快,便于用于局域网通信。

第二个参数type是Socket类型,常用的Socket类型我们之前已经介绍过了分别是SOCK_STREAM和SOCK_DGRAM因为我们要写的是TCP Socket编程所以我们使用SOCK_STREAM。

第三个参数protocol表示传输协议一般取为0。因为一般情况下有了 domain和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

调用socket函数整体代码的实现:

调用socket函数整体代码的实现:

1
int` `sockfd=socket(AF_INET,SOCK_STREAM,0);``//建立套接字

显而易见的,那么UDP的写法则为:

1
sockfd=socket(AF_INET, SOCK_DGRAM,0);

Socket编程之bind函数

在完成第一步创建套接字,分配了一个Socket描述符后,服务端的第二步就是使用在这个描述符用Bind绑定

Bind()系统调用的主要用处:

1.服务器向系统注册它的众所周知的地址。面向连接和无连接的服务器在接受客户的请求之前都必须做这一步。

2.客户可为自己注册一个特定的地址,以便服务器可以用这个有效的地址送回响应。

函数功能:

将监听套接字绑定到本地地址和端口上。

头文件:

#include <winsock2.h>

函数原型:

int bind(int sockfd, const struct sockaddr_in *addr, int addrlen);

返回值类型:

整型

返回值:

成功返回非负值,失败返回-1,最常见的错误一般是端口被占用。需要注意的是,在Linux系统中,1024以下的端口都需要root权限的程序才可以绑定

参数说明:

第一个参数sockfd为上一步创建socket时的返回值。

第二个参数addr 为 sockaddr 结构体变量的指针。该类型的定义原型如下:

1
struct sockaddr_in {  short  sin_family;  //协议族,与前面Socket函数中提到的一样,我们这里使用AF_INET``  ``u_short sin_port;    ``//端口号,需要``  ``struct` `in_addr sin_addr;  ``//IP地址,需要使用网络序``  ``char`  `sin_zero[8];  ``//没有实际意义,只是为了跟SOCKADDR结构在内存中对齐``};

第三个参数addrlen为addr 变量的大小,可由 sizeof() 计算得出。

调用bind函数整体代码的参考代码

1
struct` `sockaddr_in serv_addr  ``//创建结构体变量``servaddr.sin_family=AF_INET;  ``//sin_family指代协议族和前面讲述socket()的第一个参数的含义相同,取值也需跟socke函数第一个参数值一样。``servaddr.sin_port=htons(2000);  ``//sin_port存储端口号(使用网络字节顺序,对于htons()函数我们还有单独一章的说明,2000这个端口转换为网络字节序列。``理论上端口号的取值范围为是0到65536,但0到1023的端口一般由系统分配给特定的服务程序,比如Web 服务的端口号为 80所以我们的程序要尽量在 1024~65536 之间分配端口号。` `servaddr.sin_addr.s_addr=inet_addr(``"127.0.0.1"``);  ``//将iP地址127.0.0.1也就是本机地址转换为十进制``bind(sockfd,(sockaddr*)&servaddr,``sizeof``(servaddr));  ``// 将套接字绑定到本地地址和端口上。

Socket编程之listen函数

接着,在完成bind函数之后,服务端接下来就可以用listen函数监听了,用于监听是否有客户端连接它,以便存储多个用户的连接建立请求,listen函数具体如下:

函数功能:

让socket进入被动监听状态。什么是被动监听呢,是指当没有客户端请求时,socket处于“沉睡”中,只有当接收到客户端请求时,socket才会被“叫醒”来响应请求。

头文件:

#include <winsock2.h>

函数原型:

int listen(int sockfd, intqueue_length);

返回值类型:

整型

返回值:

成功返回0,失败返回-1

参数说明:

第一个参数为第一步sockfd创建socket时的返回值,套接字的描述符。

第二个参数queue_length用于指定接收队列的长度,也就是在Server程序调用accept函数之前最大允许进入的连接请求数,多余的连接请求将被拒绝,典型取值为5。

1
listen(sockfd,5);//监听sockfd为创建套接字时的返回值。

Socket编程之accept函数

在listen监听到有新客户端时,就可以用accept函数响应客户的连接请求,建立与客户端的连接。产生一个新的socket描述符来描述该连接,这个连接用来与发起该连接请求的客户交换数据。

函数功能:

接收客户端连接请求

头文件:

#include <winsock2.h>

函数原型:

int accept(int sockfd, struct sockaddr addr, int addrlen);

返回值类型:

整型

返回值:

成功返回非负值,失败返回-1

参数说明:

sockfd为建立socket函数返回的值。

addr为 sockaddr 结构体变量的指针,这个参数是指针类型,是向外传内容的,即addr将在函数调用后填入对方(客户端)的地址信息,如对方的IP、端口等。

addrlen为 addr变量的大小,可由 sizeof() 计算得出。

调用accept函数整体代码的实现:

struct sockaddr_in clientaddr//创建客户端地址结构体

int aID;//用来接收accept函数返回值

1
aID=accept(sockfd,(sockaddr*)&clientaddr,&``sizeof``( clientaddr));``//等待接收客户连接请求

Socket编程之recv函数

函数功能:

接收客户端或服务端传来的数据,也就是客户端和服务端都要用到

头文件:

#include <winsock2.h>

函数原型:

int recv(int aID, char *buf, int len, int flags);

返回值类型:

整型

返回值:

返回值小于0,socket报错。返回值等于0没有接收到数据,返回值大于0成功,返回值即为接收到的数据长度

参数说明:

第一个参数aID,表示连接成功的套接字描述符。

注意:这一步对于服务端而言是上一步accept的返回值;对于客户端而言是connect的返回值,并非是第一步socket创建套接字的返回值,请大家理解不要搞混!

第二个参数buf,就是为要接收的数据所在的缓冲区地址,也就是一个空的字符数组的首地址,这里放结果。

第三个参数len为要接收数据的字节数。

第四个参数flags为发送数据时的附带标记 ,一般情况下设置为0。但可以选择下列设置:

MSG_DONTROUTE:表示不使用指定路由,对send、sendto有效

MSG_PEEK:对recv, recvfrom有效,表示读出网络数据后不清除已读的数据

MSG_OOB:对发送接收都有效,表示发送或接受加急数据

调用recv函数整体代码的实现:

1
2
3
4
char recBuf[200];
//定义一个字符串用来保存客户端发来的数据
recv(aID,recBuf,200,0);
//接收来自客户端或服务端的数据

需要注意的是,recv缺省是阻塞函数,直到收到信息或出错才会返回。

Socket编程之send函数

与是recv一样,有收就会有发,发送内容对应send函数,也是从服务端accept后或客户端connect后就可以用的函数,其说明如下:

函数功能:

发送服务端或客户端的数据

头文件:

#include <winsock2.h>

函数原型:

int send(int aID, const char *buf, int len, int flags);

返回值类型:

整型

返回值:

返回值小于0,socket报错。返回值等于0对方调用了close API来关闭连接,返回值大于0成功,返回值为发送的的数据长度

参数说明:

第一个参数aID,表示连接成功的套接字描述符。

注意:这一步对于服务端而言是上一步accept的返回值;对于客户端而言是connect的返回值,并非是第一步socket创建套接字的返回值,请大家理解不要搞混!

第二个参数buf为要发送的数据所在的缓冲区地址,即一个已经存好内容的字符数组

第三个参数len为要发送的数据的实际字节数+1。

第四个参数flags为发送数据时的附带标记 ,一般情况下设置为0。但可以选择下列设置:

MSG_DONTROUTE:表示不使用指定路由,对send、sendto有效

MSG_PEEK:对recv, recvfrom有效,表示读出网络数据后不清除已读的数据

MSG_OOB:对发送接收都有效,表示发送或接受加急数据

调用send函数整体代码的实现:

1
2
char sendBuf[200];//定义一个数组用来保存发送的数据
send(aID,sendBuf,strlen(sendBuf)+1,0);//用来发送服务端或客户端的数据

与recv同样,send函数缺省也是阻塞函数,直到发送完毕或出错才会返回。

需要注意,如果函数返回值与参数len不相等,则剩余未发送的信息需要再次发送。

Socket编程之closesocket函数

一旦决定要停止通信,就要关闭套接字,释放资源,则需要调用closesocket函数进行

其函数介绍如下:

函数功能:

与socket函数功能相反关闭套接字

头文件:

#include <winsock2.h>

函数原型:

int closesocket(int aID);

返回值类型:

整型

参数说明:

aID为接收客户端请求的返回值。

调用closesocket函数整体代码的实现:

1
closesocket(aID);

这一步与第一步创建套接字一样,无论客户端还是服务端都需要进行

Socket编程之WSACleanup函数

一旦程序结束需要停止Socket库的使用,需要调用WSACleanup函数,这一步和最开始的WSAStartup是对应的。

函数功能

用于终止对So:cket字库的使用。

库链接:

#pragma comment(lib,"ws2_32.lib")表示链接Ws2_32.lib这个库。 ws2_32.lib是Winsock2的库文件。

函数原型:

int PASCAL FAR WSACleanup (void)。

返回值类型:

整型

返回值:

成功返回0

参数说明:

无参数

因此调用WSACleanup函数也很简单,如下:

1
WSACleanup();

Socket服务端完整参考代码

前面讲解了Socket通信中服务端的每一步功能作用及实现,而重点是多个步骤在一起时,上下文 函数之间的信息传递需要我们理解,如SOCKADDR_IN的结构体、各个SOCKET描述符等参数,以及在此基础之上改进得到的希望的实际效果(如需要不停的接受消息、发送消息)

下面我们将所有步骤串联在一起,提供一个可以连续接收客户端信息的服务端程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{

WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData); //目前建议使用最新2.2版本
SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//创建了可识别套接字
if(serSocket!=-1)
{
printf("成功创建套接字!%d\n",serSocket);
}

//需要绑定的参数,主要是本地的socket的一些信息。
SOCKADDR_IN addr;
addr.sin_family=AF_INET;
addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//ip地址
addr.sin_port=htons(12345);//绑定端口

bind(serSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR));//绑定完成
listen(serSocket,5);//其中第二个参数代表能够接收的最多的连接数
printf("等待客户端...\n");
SOCKADDR_IN clientsocket;
int len=sizeof(SOCKADDR);
//第二次握手,通过accept来接受对方的套接字的信息
SOCKET serConn=accept(serSocket,(SOCKADDR*)&clientsocket,&len);
//如果这里不是accept而是conection的话。。就会不断的监听
if(serConn)
{
printf("监听到新的客户端...\n");
}
while (1)
{
char sendBuf[100];
sprintf(sendBuf,"welcome %s to here",inet_ntoa(clientsocket.sin_addr));//找对对应的IP并且将这行字打印到那里
//发送信息
send(serConn,sendBuf,strlen(sendBuf)+1,0);
char receiveBuf[100];//接收
int RecvLen;
RecvLen=recv(serConn,receiveBuf,100,0);
if(RecvLen!=-1)
printf("%d %s\n",RecvLen,receiveBuf);
else
break;

}
closesocket(serConn);//关闭
WSACleanup();//释放资源的操作
return 0;
}

因为暂时没有客户端连接,运行后为等待效果状态.

TCP的客户端代码流程简述

这一章将为大家讲解Socket通信中客户端的实现过程,还是先上图,请大家了解客户端的步骤

Socket通信客户端

可以看到,相比服务端,客户端的步骤简单的很多。事实上这种情况比较多,比如一个服务端会有多个客户端连接。

通过图片我们可以看到TCP客务端调用的函数依次是socket( )、connect( )、recv( )、send( )、closessocket( )

由于在服务端这章的讲解中我们提到socket()[recv()]、send()、closesocket()、WSAStartup()、WSACleanup()的函数,在客户端中同样需要这些函数,使用方式是一样的,因此这里不再赘述。

Socket编程之connect函数

这一节我们讲connect连接,这一步位于客户端的第二步,调用connect阻塞客户程序,传输层实体开始建立连接,当连接建立完成时,取消阻塞;

函数功能:

向服务端发起连接请求

头文件:

#include <winsock2.h>

函数原型:

int connect(int sockcd, const struct sockaddr *addr, int addrlen);

返回值类型:

整型

返回值:

成功返回0,失败返回-1。当客户端调用 connect()函数之后,发生以下情况之一才会返回(完成函数调用)

1、服务器端接收连接请求

2、发生断网的异常情况而终端连接请求

参数说明:

sockcd为客户端建立socket函数的返回值。

addr是一个sockaddr结构的指针,用于指定所要连接的服务器的地址(服务端的IP地址和端口号,要和服务端的实际IP地址以及绑定的端口一致才可以)。

addrlen为addr变量的大小,可由 sizeof()计算得出。

调用connect函数整体代码的实现:

1
2
connect(sockcd,(sockaddr*)&seraddr,sizeof(seraddr));
//需要注意的是,所谓的“接收连接”并不意味着服务器调用

accept()函数,其实是服务器端把连接请求信息记录到等待队列。因此connect()函数返回后并不进行数据交换。而是要等服务器端 accept 之后才能进行数据交换。

这一步调用完成之后,就和服务端建立了通信,就可以使用send或recv相互发送和接收消息了

Socket客户端完整参考代码

本代码用于和第二章服务端代码一致,监听12345端口,可以不断的发送消息,直至输入"quit"退出程序,完整参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")

int main()
{
int err;
char SendBuf[100];
WORD versionRequired;
WSADATA wsaData;
versionRequired=MAKEWORD(2,2);
err=WSAStartup(versionRequired,&wsaData);//协议库的版本信息
//通过WSACleanup的返回值来确定socket协议是否启动
if (!err)
{
printf("客户端套接字已经打开!\n");
}
else
{
printf("客户端套接字打开失败!\n");
return -1;//结束
}
//注意socket这个函数,他三个参数定义了socket的所处的系统,socket的类型,以及一些其他信息
SOCKET clientSocket=socket(AF_INET,SOCK_STREAM,0);

//socket编程中,它定义了一个结构体SOCKADDR_IN来存计算机的一些信息,像socket的系统,
//端口号,ip地址等信息,这里存储的是服务器端的计算机的信息
SOCKADDR_IN clientsock_in;
clientsock_in.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
clientsock_in.sin_family=AF_INET;
clientsock_in.sin_port=htons(12345);

//前期定义了套接字,定义了服务器端的计算机的一些信息存储在clientsock_in中,
//准备工作完成后,然后开始将这个套接字链接到远程的计算机
//也就是第一次握手
int r=connect(clientSocket,(SOCKADDR*)&clientsock_in,sizeof(SOCKADDR));//开始连接
// printf("%d\n",r);

while(1)
{
gets(SendBuf);
if(strcmp(SendBuf,"quit")==0)
break;
send(clientSocket,SendBuf,strlen(SendBuf)+1,0);
}


closesocket(clientSocket);
//关闭服务
WSACleanup();
return 0;
}

单独运行客户端,只会有客户端套接字已经打开

若是连同前面的服务端一起测试,先运行服务端,再运行客户端,即可完成通信效果。

客户端向服务端发送三条消息,服务端都已接收,并打印长度和消息信息,第四条信息退出,之后双方退出结束程序

什么是字节序?大小端还有网络序和主机序?

1.字节序

字节序,又称端序或尾序,指的是多字节数据在内存中的存放顺序。学过C语言后,我们知道一个int型变量a是占用4个字节,假设它的起始地址也就是&a是0x10处,那么变量a的四个字节将会被存储在0x10、0x11、0x12和0x13这四个字节位置上。

但是当我们写好通信程序发送数据时候的时候,这个a变量通过TCP连接传输后收到的与发送的不一致,即有可能发过去的序列变成了0x12、0x13的值在前,0x10、0x11上的值在后,这样组成的四个字节的int类型值肯定就不一样了。

所以要引入大端和小端的概念。

2.大端和小端

计算机有两种储存数据的方式:大端字节序(Big Endian)和小端字节序(Little Endian)。

大端模式:是指数据的高字节保存在内存的低地址中,低字节保存在内存的高地址端。

小端模式:是指数据的高字节保存在内存的高地址中,低字节保存在内存的低地址端。

以一个两字节short型变量0x0102的存储举例:

大端字节序:高位字节在前,低位字节在后,01|02,从左往右看着更习惯。

小端字节序:低位字节在前,高位字节在后,02|01,也存在这种存储顺序。

我们以0x12345678这个数字为例,它的大端模式和小端模式分别如下:

大小端存储顺序

3.原因?

计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节…

如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节;小端字节序正好相反。

如果这样,那统一用符合我们人类读写习惯的大端序就好了呀,为何还要弄出个小端序了?

这是疑问计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的,所以计算机的内部处理都是小端字节序。

但是人类还是习惯读写大端字节序,所以除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

4.网络序和主机序

明白了大小端之后,网络序和主机序也就好理解了,

网络字节序:TCP/IP各层协议将字节序定义为Big Endian,即大端模式,TCP/IP协议中使用的字节序是大端序。

主机字节序:整数在内存中存储的顺序,目前以Little Endian,即小端模式,比较普遍(不同的CPU有不同的字节序)。

C/C++语言编写的程序里数据存储顺序是跟编译平台所在的CPU相关的,而现在比较普遍的x86处理器是小端模式(Little Endian)。Java编写的程序则唯一采用Big Endian方式来存储数据。

所以,如果你的C/C++程序通过Socket将变量a = 0x12345678的首地址传递给了Java程序,由于Java采取Big Endian方式存取数据,很显然,本地数据没问题,传过去就变成0x78563412,这就出问题了。毕竟不是所有的客户端和服务端都是同一种语言、同一种CPU。因此转换的问题就来了

5.如何转换

为避免开头说到的网络通信中存在的问题,我们可以在传输数据之前和接收数据之后对数据进行相应处理,也就是主机序和网络序的转换。

htos和htol函数:主机序转换到网络序

在网络传输过程中,一定会涉及到主机序和网络序的问题,即本机的存储和网络的传输是完全两套存储方式,我们保证不了目标主机的字节序是否和网络序一致,因此一定要考虑这个问题,这里介绍常用的两个函数htos和htol函数,使主机序转换到网络序

1.htos函数:

函数功能:

将主机无符号短整形数转换成网络,比如古人读12345的顺序是从右往左54321,而现代人读12345的顺序是从左往右读12345,htos函数就是完成类似的转换功能,举例说明如果把htons(16)输出你会看到得到的结果是4096,为什么呢?因为16的十六进制是0X0010,而4096的十六进制是0X1000。不同的存储方式,会导致高低位存储时顺序的不同,这就是即00 10和10 00 的存储不同的原因。

头文件:

#include <winsock2.h>

函数原型:

uint16_t htons(uint16_t hostlong);

返回值类型:

整型

返回值:

返回一个网络字节顺序的值

参数说明:

其中hostlong是主机字节顺序表达的16位数,htons中的h表示host意思是主机地址,to表示to意思是去往,转换为的意思,n表示net意思是网络,s表示signed long意思是无符号的短整型。

调用htos函数代码举例;

1
htos(5200);

2.htol函数

函数功能:

将一个32位数从主机字节顺序转换成网络字节顺序。

头文件:

#include <winsock2.h>

函数原型:

uint16_t htons(uint32_t hostlong);

返回值类型:

整型

返回值:

返回一个网络字节顺序的值

参数说明:

其中hostlong是主机字节顺序表达的32位数,htons中的h表示host意思是主机地址,to表示to意思是去往,转换为的意思,n表示net意思是网络,l 是 unsigned long表示32位长整数

调用htol函数代码举例;

1
htol( 0x403214);

ntohl和ntohs函数:网络序转换到主机序

有主机序转网络序,就有网络序转主机序,分别是ntohl和ntohs函数,接下来为大家讲解这两个函数。

1.ntohl函数

函数功能:

将一个无符号短整型数从网络字节顺序转换成主机字节顺序。这个函数与htons原理相同,不过是htos是主机序到网络序,而ntohs是网络序到主机序。

头文件:

#include <winsock2.h>

函数原型:

uint16_t ntohs(uint16_t netshort);

返回值类型:

整型

返回值:

返回一个主机字节顺序表达的数。

参数说明:

其中netshort一个以网络字节顺序表达的16位数,ntohs中的h表示host意思是主机地址,to表示to意思是去往,n表示net意思是网络,s表示signed long意思是无符号的短整型(32位的系统是2字节)。

调用ntohs函数代码举例;

1
ntohs(5200);

2.ntohl函数

函数功能:

将一个无符号长整型从网络字节顺序转换成主机字节顺序。这个函数与htonl原理相同,不过是htol是主机序到网络序,而ntohl是网络序到主机序。

头文件:

#include <winsock2.h>

函数原型:

uint16_t ntohs(uint16_t netlong);

返回值类型:

整型

返回值:

返回一个主机字节顺序表达的数。

参数说明:

其中netlong一个以网络字节顺序表达的32位数,ntohs中的h表示host意思是主机地址,to表示to意思是去往,n表示net意思是网络,s表示signed long意思是无符号的短整型(32位的系统是2字节)。

调用ntohl函数代码举例;

1
ntohl( 0x403214);

Sockaddr_in和Sockaddr的区别

sockaddr和sockaddr_in都是结构体,并且它们的功能都是用来处理网络通信的地址。网络中的地址主要有3个方面的属性:

1、地址类型例如是互联网协议第四版(ipv4)和互联网协议第六版(ipv6)。

2、IP地址,主要有5类分别是A类:(1.0.0.0-126.0.0.0),地址的网络号取值于1~126之间。一般用于大型网络。

B类:(128.0.0.0-191.255.0.0),地址的网络号取值于128~191之间。一般用于中等规模网络。

C类:(192.0.0.0-223.255.255.0),地址的网络号取值于192~223之间。一般用于小型网络。

D类:是多播地址,地址的网络号取值于224~239之间。一般用于多路广播用户 。

E类:是保留地址,地址的网络号取值于240~255之间。

3、端口,它就像门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端有很多端口,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号,范围是0---65535。

用于存储参与(IP)Windows套接字通信的计算机上的一个internet协议(IP)地址。为了统一地址结构的表示方法 ,统一接口函数,使得不同的地址结构可以被bind()、connect()、recv()、send()等函数调用。但一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in。这是由于Microsoft TCP/IP套接字开发人员的工具箱仅支持internet地址字段,而实际填充字段的每一部分则遵循sockaddr_in数据结构,两者大小都是16字节,所以二者之间可以进行切换。

sockaddr_in中的in就表示internet也就是网络地址的意思,它弥补了sockaddr的缺陷,把port(端口号),和addr(目标地址)分开存储在两个变量中。

总结

二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。

sockaddr常用于bind、connect、recv、send等函数的参数,指明地址信息,是一种通用的套接字地址。

sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用强制类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。