[网络]——编程套接字和BSD相关API

mac2024-08-20  60

终于完成了linux的几种设计模式来到了网络的章节,在这里我们可以尝试实现TCP,UDP的c/s服务器,不过在这之前,首先了解网络编程的一些基础概念。

套接字

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。 ----百度百科

在操作系统中,通常会为应用程序提供一组应用程序接口(API),称为套接字接口(英语:socket API)。应用程序可以通过套接字接口,来使用网络套接字,以进行数据交换。最早的套接字接口来自于4.2 BSD,因此现代常见的套接字接口大多源自Berkeley套接字(Berkeley sockets)标准。在套接字接口中,以IP地址及通信端口组成套接字地址(socket address)。远程的套接字地址,以及本地的套接字地址完成连线后,再加上使用的协议(protocol),这个五元组(five-element tuple),作为套接字对(socket pairs),之后就可以彼此交换数据。例如,在同一台计算机上,TCP协议与UDP协议可以同时使用相同的port而互不干扰。 操作系统根据套接字地址,可以决定应该将数据送达特定的行程或线程。这就像是电话系统中,以电话号码加上分机号码,来决定通话对象一般。----维基百科

端口号

端口号属于传输层协议

端口号是是一个2字节十六位的整数端口号用于标识一个进程,告诉操作系统,当前这个数据交给哪一个进程来处理IP+端口号能够标识网络上的某台主机的某一个进程一个端口号可以被一个进程占用

我们在之前学了进程ID也能标识唯一的一个进程,它和端口号有什么区别? 一个进程可以有多个端口号,但是一个端口号只能绑定一个进程 举个例子,我是张三,有两个电话号码,你是李四,只有一个电话号码,有一天我用其中一个电话号码给你打电话,这就相当于进程通信。 另外进程ID是由操作系统内核进行分配管理的,端口号是由通讯协议分配并管理的,简单的说这两个没有关系。

认识TCP协议和UDP协议

这里我们先对TCP协议和UDP协议的性质进行介绍。 TCP

传输层协议

面向连接

可靠传输

面向字节流 UDP

传输层协议

无连接

不可靠传输

面向数据报

这里我们不对TCP和UDP做过多的解释,在以后的博客里会介绍它们俩,只有两点要注意,什么是面向数据报和面向字节流,UDP面向数据报意味着应用层一次传给UDP多少报文字节,UDP就向底层发送多少报文字节,如果过大,底层的IP会进行分片减少效率。 TCP是面向字节流的,应用程序每次和TCP交换一个大小不等的字节流,TCP有一个缓冲,当传入的数据太长时可以划分短一些再传走,如果传入的数据太短,亦可以等积累的数据足够,再构成报文段发送。

面向连接和无连接的区别可以看这篇博客理解面向连接和无连接 对无连接协议来说,每个分组的处理都独立于所有其他分组,而对面向连接的协议来说,协议实现则维护了与后继分组有关的状态信息。

网络字节序

我们在前面学习了大小端的区别,对于一台主机内的进程进行通信还没有问题,但是到了网络上假如一个小端的机器给大端机器发送信息,假如不改变,那信息岂不是来奶来,因此我们要定义网络字节流的地址。

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

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

看着难记,h指host,n指network,l指32位长整数,s指16位短整数. htonl函数就是将主机的字节转换为网络的字节序。 如果主机是小端字节序,这些函数会将参数进行大小端转换然后返回。 如果主机是大端字节序,这些函数不进行转换,将参数原封不动的返回。

socke编程接口API

下面是socket常见的API,我们可以一个一个看。

int socket (int domain.int type,int protocol); 创建socket 文件描述符。

有三个参数 domain 为创建的套接字指定协议集(或称做地址族 address family)。 例如:

AF_INET 表示IPv4网络协议AF_INET6 表示IPv6AF_UNIX 表示本地套接字(使用一个文件)

type(socket类型)如下:

SOCK_STREAM (可靠的面向流服务或流套接字)SOCK_DGRAM (数据报文服务或者数据报文套接字)SOCK_SEQPACKET (可靠的连续数据包服务)SOCK_RAW (在网络层之上自行指定运输层协议头,即原始套接字)

protocol 指定实际使用的传输协议。

IPPROTO_TCP、IPPROTO_SCTP、IPPROTO_UDP、IPPROTO_DCCP。这些协议都在<netinet/in.h>中有详细说明。 如果该项为“0”的话,即根据选定的domain和type选择使用缺省协议。

如果发生错误,函数返回值为-1。 否则,函数会返回一个代表新分配的描述符的整数。 传入的返回值相当于一个文件操作符,我们可以对其进行write,close等操作,通信双方通过该socket进行通讯操作。

int bind(int socket,const struct sockaddr *address my_addr,socklen_t address_len) 绑定端口号

socket, 表示使用bind函数的套接字描述符my_addr, 指向sockaddr结构(用于表示所分配地址)的指针addrlen, 用socklen_t字段指定了sockaddr结构的长度如果发生错误,函数返回值为-1,否则为0。

int listen(int sockfd, int backlog);

当socket和一个地址绑定之后,listen()函数会开始监听可能的连接请求。然而,这只能在有可靠数据流保证的时候使用,例如:数据类型(SOCK_STREAM, SOCK_SEQPACKET)。

sockfd, 一个socket的描述符.backlog, 完成三次握手、等待accept的全连接的队列的最大长度上限。(这里只说这一部分,可以暂时理解为每一个连入请求都要进入一个连入请求队列,等待listen 的程序调用accept()函数来接受这个连接。当系统还没有 调用accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是backlog 的数值。) 一旦连接被接受,返回0表示成功,错误返回-1。

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

connect()系统调用为一个套接字设置连接,参数有文件描述符和主机地址。 某些类型的套接字是无连接的,大多数是UDP协议。对于这些套接字,连接时这样的:默认发送和接收数据的主机由给定的地址确定,可以使用 send()和 recv()。 返回-1表示出错,0表示成功。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

当应用程序监听来自其他主机的面对数据流的连接时,通过事件(比如Unix select()系统调用)通知它。必须用 accept()函数初始化连接。 Accept() 为每个连接创立新的套接字并从监听队列中移除这个连接。它使用如下参数:

sockfd,监听的套接字描述符cliaddr, 指向sockaddr 结构体的指针,客户机地址信息。addrlen,指向 socklen_t的指针,确定客户机地址结构体的大小 。返回新的套接字描述符,出错返回-1。进一步的通信必须通过这个套接字。

Datagram 套接字不要求用accept()处理,因为接收方可能用监听套接字立即处理这个请求。

sockaddr结构体

struct sockaddr和struct sockaddr_in这两个结构体用来处理网络通信的地址。

在各种系统调用或者函数中,只要和网络地址打交道,就得用到这两个结构体。

struct sockaddr { unsigned short sa_family; //2 char sa_data[14]; //14 };

上面是通用的socket地址,具体到Internet socket,用下面的结构,二者可以进行类型转换

struct sockaddr_in { short int sin_family; //2 unsigned short int sin_port; //2 struct in_addr sin_addr;//4 unsigned char sin_zero[8]; //8 };

struct in_addr就是32位IP地址。

struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; #define s_addr S_un.S_addr };

注释中标明了属性的含义及其字节大小,这两个结构体一样大,都是16个字节,而且都有family属性,不同的是:

sockaddr用其余14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。

说一个题外话,事实上因为vod指针可以转换为任意类型的指针,接口上的sockaddr指针类型可以修改为void类型,但是为什么现在没有这么修改参数? 原因是因为当时有这套API的时候,还没有void类型,到后面虽然有了,但是已经习惯这么使用,约定俗成了,因此也没有改变。

地址转换函数

sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址,但是我们通常使用点分十进制的字符串表示IP地址,以下函数用于字符串和in_addr表示之间转换. 这里我推荐看这一篇博客 地址转换函数inet_addr(), inet_aton(), inet_ntoa()和inet_ntop(), inet_pton() 讲的比较细。

看完了上面的接口,就可以尝试完成c/sUDP服务器和客户端和TCP服务器/客户端。 在下一篇博客将尝试实现。

最新回复(0)