网络编程套接字

时间:2024-04-19 07:21:31

1.源IP和目的IP

在网络通信中,我们是将数据从一台主机发送到另一台主机上,而IP就是用于标识网络中的哪一台主机。就像生活中寄快递一样,我们给别人寄快递时,快递单号上会有,快递的源地址和目的地址。网络通信也是一样,需要有源IP和目的IP。

2.端口号

当我们可以找到对端主机时,又出现了一个问题,应该将该数据交给哪一个进程(软件),比如,你打开了抖音时,抖音的服务端会将数据发送给抖音的客户端,而不是其他的软件。所以我们还需要一个变量标识主机上的某一个进程。

我们规定使用端口号port标识服务端进程,客户端进程的唯一性。

端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用
  1. IP地址(标识唯一一台主机)+ 端口号(标识唯一一个进程)就能标识全网指定一台主机上的指定的进程。
  2. 网络通信的本质其实就是进程间通信,进程间通信的本质是让两个进程看见同一份资源,现在这个资源就是网络

2.1进程pid和端口号port

端口号是用来标识进程的唯一性的,但是进程pid不也是标识进程唯一性的吗,为什么不能用pid来充当网络中标识进程唯一性的符号呢。

  • 1.pid是系统的概念,而port是网络的概念,这样可以让网络和系统在一定程度上解耦。
  • 2.并不是每一个进程都会进行网络服务,不进行网络服务的进程就可以不用分配port
  • 3.进程每次重启,pid都不相同,但是网络服务中,如果使用pid,客户就无法找到服务器。就像电话中的110,120一样

2.2理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";

所以源IP+源端口,目的IP和目的端口,有了这四个信息,我们就可以将数据从一个进程发送给另一个进程(进程间通信)。

但是有人可能会问,我平时上网的时候,也从来没有输入过源IP,源端口,目的IP,目的端口这四个中的任何一个,那我是如果获取数据的呢,以抖音举例,抖音平台在开发时,会开发客服端和服务端,并且在内部以及内置好了,让客服端知道服务端的IP和端口。我们下载好抖音并打开的时候,就会将我们手机的IP和port也发送给服务端,就这样,双方都知道了对方的IP和port,未来就可以正常通信了。

3.TCP/UDP协议

上面说到,网络通信必须知道源IP和目的IP,而传输层就是帮我们封装源IP和目的IP的一层。传输层有两个重要的协议:TCP和UDP

当应用层将数据交给传输层时,TCP和UDP协议会帮我们封装好源IP和目的IP。

3.1TCP特点

  1. 传输层协议
  2. 有连接
  3. 可靠传输
  4. 面向字节流

3.2UDP特点

  1. 传输层协议
  2. 无连接
  3. 不可靠传输
  4. 面向数据报

可靠与不可靠:

可靠传输表示当数据出现丢包,网络出现问题等问题,也能将数据成功送达到对端主机(通过各种手段,例如重传等)

注意这里的可靠不是褒义词,不可靠也不是贬义词,而是中性词,TCP为了维护可靠性,他必须要在性能,成本上做出牺牲。UDP不可靠,意味着他的成本更低,效率更高,更简单。具体使用哪种协议要分场景。如网络上发送信息,就不能出现丢包等问题,要使用TCP协议;在观看直播时,偶尔出现数据丢包的问题也无伤大雅,就可以考虑使用UDP协议。

4.网络字节序

我们知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。

大端:低字节序放在高地址,高字节序放在低地址

小端:高字节序放在低地址,低字节序放在高地址

如果网络数据流使用不同的大小端,那么可能会出现一个很尴尬的问题,一个主机使用的是小端,他将数据以小端的形式发送到对端,对端是一个大端机,以大端的方式读取,那么独到的可能就是两个不同的数。

为了避免这个问题,我们规定,网络中的数据统一采用大端的形式。如果发送端是大端,直接发送,如果是小端,先将数据转成大端再发送。

如何定义网络数据流的地址呢

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>

//主机序列转网络序列
uint32_t htonl(uint32_t hostlong);
uint32_t htons(uint16_t hostshort);
//网络序列转主机序列
uint32_t ntohl(uint32_t netlong);
uint32_t ntohs(uint16_t netshort);
  1. 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  2. 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  3. 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ;
  4. 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

5.socket编程接口

IP+端口标识全网唯一进程,这里我们将IP+端口的组合叫做套接字Socket。

Socket编程是一种网络编程的形式,通过网络套接字(socket)实现进程之间的通信。它可以在不同的计算机之间或同一台计算机上的不同进程之间传递数据。Socket编程使用了客户端-服务器模型,其中一个程序作为服务器端监听特定的端口,而其他程序则作为客户端与服务器进行通信。

Socket编程基于TCP/IP协议栈,通过使用套接字(socket)实现数据传输。具体来说,服务器端开始监听绑定的端口,等待客户端的连接请求。

总的来说,Socket编程是网络编程中至关重要的一部分,它提供了一种在不同主机之间进行数据通信的方式。对于希望进行网络通信的应用程序来说,掌握Socket编程是非常必要的。

5.1Socket接口

创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

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

绑定端口号 (TCP/UDP, 服务器)

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

开始监听socket (TCP, 服务器)

int listen(int socket, int backlog);

接收请求 (TCP, 服务器)

int accept(int socket, struct sockaddr* address, socklen_t* address_len);

建立连接 (TCP, 客户端)

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

5.2sockaddr结构

我们先说一下套接字的类型:

  • 网络套接字(跨主机之间的通信,也支持本地通信)  sockaddr_in
  • 域间套接字(只能本地通信)                                      sockaddr_un
  • 原始套接字(跨传输层直接访问底层数据)                sockaddr

在上面那些结构中,我们可以看到有很多接口中都有一个参数是sockaddr。网络通信中是有不同的场景的,但是针对不同的场景要设计出几套类似的接口吗。可以但是没必要,于是就出现了sockaddr结构。通过传入不同的参数,对应到不同的场景。

当我们传参时,不用指定传sockadrr_insockaddr_un。而是全部都传sockaddr。在设置参数时,我们只需要设置协议家族(前16个bit),是本地通信还是网络通信,即可区分是哪一种套接字。在socket函数内部会自动提取前16个bit位。

现在我们要进行socket编程,虽然函数的参数都是const struct sockaddr *addr,而我们创建的实际上是sockaddr_in结构体,只需要在传参时,进行强制类型转换即可。在函数内部会自动判断提取前16个bit位,判断是什么类型,再强转回去。

  • 为什么不使用void* 替换 sockaddr*。

我们可以想到C语言中的void*也可以成功应用上面的场景,并且很多函数接口也是这样设计的,但是为什么这里没有使用void* 替换 sockaddr*呢。其实也很简单:在设计这套接口的时候,C语言还不支持void*。

5.3总结

  • 我们可以将sockaddr看成基类,sockaddr_in和sockaddr_un看成子类,构成了多态。
  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;