当前位置:   article > 正文

【Unix 网络编程】TCP Socket 编程基础(0)_socket函数 tcp

socket函数 tcp

一、套接口地址结构

 IPv4 套接口地址结构

  1. struct in_addr{
  2. in_addr_t s_addr; /*32-bit IPV4地址*/
  3. }; /*网络字节序*/
  4. struct sockaddr_in
  5. {
  6. uint8_t sin_len; /**/
  7. sa_family_t sin_family; /*地址族*/
  8. in_port_t sin_port; /*16-bit TCP或UDP 端口号*/
  9. /*网络字节序*/
  10. struct in_addr sin_addr; /*32-bit IPv4 地址*/
  11. /*网络字节序*/
  12. char sin_zero[8]; /*暂不使用*/
  13. };
POSIX 规范只需要这个结构中的 3 个成员:sin_family,sin_port 和 sin_addr。

套接口地址结构仅在给定主机上使用:虽然结构中的某些成员(如 IP 地址和端口号)用在不同主机间的通信中,但结构本身并不参与通信。

当作为参数传递给任一个套接口函数时,套接口地址结构总是通过指针来传递,但通过指针来取得此参数的套接口函数必须处理来自所支持的任何协议族的套接口地址结构。这类似于C++中的父类子类思想(is-a),因为一个操作不同协议族的套接口地址都是通过同一个套接口函数来处理的,这就需要一个通用套接口地址结构指针来指向(C++中多态,父类指针指向子类对象)。实际上将套接口函数的形参设置为 void* 更简单,但是套接口函数是在 ANSI C 之前定义的。

通用套接口地址结构

  1. struct sockaddr
  2. {
  3. uint8_t sa_len;
  4. sa_family_t sa_family; /*地址族*/
  5. char sa_data[14]; /*14字节协议地址,包含套接字中的目标地址和端口信息*/
  6. };
通用套接口地址结构的用途就是为了对特殊协议的地址结构进行强制转换,于是套接口函数被定义为采用指向通用套接口地址结构的指针,所以对这些套接口函数任何调用都必须将指向特定协议的套接口地址结构的指针类型转换成指向通用套接口地址结构的指针。

从内核的角度看,使用指向通用套接口地址结构指针的原因是:内核必须依据调用者的指针,将其转换为 struct sockaddr * 类型,然后检查 sa_family 的值来确定结构的类型。但从应用程序开发人员的角度看,指针类型为 void * 更简单,不需要进行明确的类型转换。

二、套接口函数

1. socket 函数

为了执行网络I/O,一个进程必须做的第一件事就是调用 socket 函数,指定期望的通信协议类型。

  1. #include <sys/socket.h>
  2. int socket(int family, int type, int protocol);
  3. /*返回:非负描述字——成功, -1——出错
  4. 其中family参数指明协议族,type参数指明套接口类型,后面protocol通常设为0,以选择所给定family 和 type组合的系统缺省值*/

socket 描述符(套接字)是一个指向内部数据结构的指针,它指向描述符表入口。调用 socket 函数时,Socket 执行体将建立一个 Socket,实际上 “建立一个Socket” 意味着为一个 Socket 数据结构分配存储空间。Socket 执行体为你管理描述符表。套接字是比较笼统的,在其创建之后使用之前还必须调用其余过程来填充这些字段(套接字描述符表)

两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。Socket 数据结构中包含这五种信息。

2. connect 函数

TCP 客户用 connect 函数来建立与 TCP 服务器(指定IP地址和端口号)的连接

  1. #include <sys/socket.h>
  2. int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
  3. /*sockfd是由socket函数返回的套接口描述字,第二、第三个参数分别是一个指向套接口地址结构的指针
  4. 和该结构的大小。套接口地址结构必须含有服务器的IP地址和端口号*/

上面的 sockfd 套接字描述字是客户端的套接字。面向连接的 socket 客户端通过调用 connect 函数在 socket 数据结构中保存本地和远端信息

第一个参数 sockfd 是由socket函数创建的套接字,只含有本地IP和端口号信息,而 connect 函数中则传入有目的IP和端口号信息,在 connect 函数成功建立连接后,便将目的IP和端口号信息写入了这个 socket 描述符所描述的 socket 对象中。这样这个 socket 便同时记录了本地和目的的IP和端口号信息。于是该 socket 可用于客户/服务器之间的通信。

3. bind 函数

bind 函数将 socket 与本机上的一个端口相关联,即把一个本地协议地址赋予一个套接字。对于TCP,在调用 socket 函数创建 socket 是,内核并未给 socket 分配IP地址和端口号。这个 bind 绑定端口是将本来没有指定端口的 socket 绑定到我们指定的端口上,而不是将一个已经分配了端口的 socket 重定向到我们制定的端口上。如果未 bind 的话,当调用 connect 和 listen 时,系统就要为相应的套接字选择一个临时接口。

  1. #include <sys/socket.h>
  2. int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
  3. /*sockfd是由socket函数返回的套接口描述字,第二个参数是一个指向特定于协议的地址结构的指针,
  4. 第三个参数是该地址结构的长度*/

上面的第二个参数是一个指向 sockaddr 的指针,不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in)来代替:

  1. #include <unix/in.h>
  2. struct sockaddr_in{
  3. unsigned short sin_family;
  4. unsigned short int sin_port;
  5. struct_addr sin_addr;
  6. unsigned char sin_zero[8];
  7. };
在函数调用时,进行强制转换。

服务器在启动时捆绑它们的众所周知的端口,如果一个TCP客户或服务器未曾调用 bind 捆绑一个端口,当调用 connect 或 listen 时,内核就要为相应的套接口选择一个临时端口。让内核来选择临时端口对于TCP客户来说是正常的,除非应用需要一个预留端口,但是服务器却不是这样,因为服务器与客户是一个一对多的关系,是通过它们的众所周知端口被大家认识的。

4. listen 函数

listen 函数仅有TCP服务器调用。 当 socket 函数创建一个套接口时,它被假设为一个主动套接口,是一个将调用connect 发起连接的客户套接口。listen 函数把一个未连接的套接口转换成一个被动套接口,指示内核应接受指向该套接口的连接要求。

  1. #include <sys/socket.h>
  2. int listen(int sockfd, int backlog);
  3. /*sockfd是bind之后的套接口描述字,第二个参数规定了内核应该为相应套接口排队的最大连接个数*/

服务端调用该函数,表示开始监听客户端的连接请求,listen 只是负责监听连接请求,而不负责接受客户端的连接请求(accept 接受)。

5. accept 函数

accept 函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。也就是说 accept 函数的功能就是等待并接受客户的连接,它从内核中取出已经建立的客户连接,然后把这个已经建立的连接返回给用户程序,此时用户程序就可以与自己的客户进行点对点的通信了。accept 函数等待并接受客户的请求。其默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。

  1. #include <sys/socket.h>
  2. int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
  3. //返回:非负描述子——成功,-1——出错
  4. /*参数sockfd是监听后的套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用一个与这个套接字关联的端口号,
  5. 比较特别的是:参数cliaddr和addrlen是一个结果参数,用来返回已连接客户的协议地址。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL*/

cliaddr 用于存放客户端的地址结构,addrlen 在调用函数时被设置为 cliaddr 指向区域的长度,在函数调用结束后被设置为实际地址信息的长度。

返回值是一个新的套接字,它代表的是和客户端的新的连接,可以把它理解成是一个客户端的 socket ,这个 socket 包含的是客户端的 IP 和 端口号信息。当然这个新的 socket 会从第一个参数socket中继承服务器的 IP 和端口号信息,这样这个新的 socket 就具备客户和服务器两种端口信息(实际上,socket 结构体不仅记录了本地的IP和端口号,还记录了目的IP和端口号)

如果 accept 成功,那么其返回值是由内核自动生成的一个全新描述字,代表与所返回客户的TCP连接,而这个全新套接字则记录有本地和目的的IP和端口号信息,可以用于通信。

accept 函数中,我们称它的第一个参数为监听套接口描述字(由 socket 创建,随后用作 bind 和 listen 的第一个参数的描述字),称它的返回值为已连接套接口描述字。

这 两个套接字需要区分,一个服务器通常仅仅创建一个监听套接口,它在该服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接套接口(也就是说对于它的TCP三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接口就被关闭。

相当于在服务器的生命期内会一直监听客户的申请,当有客户申请并完成三路TCP握手后,内核会自动创建一个已连接套接口,这个与服务器无关。

之前提到了 accept  函数最多可返回三个值:已连接套接口描述字,客户进程的协议地址以及该地址的大小。

如果 accept 成功返回,则服务器与客户的已经正确建立连接了,此时服务器通过 accept 返回的套接字来完成与客户的通信。

首先,当 accept 函数监视的 socket 收到连接请求时,socket 执行体将建立一个新的 socket,执行体将这个新的 socket 和请求连接进程的地址联系起来,收到服务请求的初始 socket 仍可以继续在以前的 socket 上监听,同时可以在新的 socket 描述符上进行数据传输操作。

需要额外指出的是,新创建的已连接套接字与监听套接字使用的是一样的端口号,即已连接套接字并未占用新的端口与客户端通信。

6. close 函数

close 函数也可用来关闭套接字,并终止TCP连接。

  1. #include<unistd.h>
  2. int close(int sockfd);
  3. /*返回:0——成功,-1——出错*/
当所有的数据操作结束以后,你可以调用 close 函数来释放该 socket,从而停止在该 socket 上的任何数据操作。

二、IO 函数

send 和 recv 这两个函数用于面向连接的 socket 上进行数据传输

  1. #include <sys/socket.h>
  2. ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
  3. ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
  4. /*返回:读入或写出的字节数——成功,-1——出错*/
上面的第一个参数 sockfd 是连接建立后的套接字,既记录了本地的IP和端口号信息,又记录了目的的IP和端口号信息。

三、字节排序函数

我们把某个给定系统所用的字节序称为主机字节序,网络协议必须指定一个网络字节序,网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节序采用大端存储方式,主机字节序存储方式因不同的CPU上运行不同的系统而不同,这样在网络协议中就需要将主机字节序转换为网络字节序,不然通信两方的解释会因不一样而产生bug。

  1. #include <netinet/in.h>
  2. uint16_t htons(uint16_t host16bitvalue);
  3. uint32_t htonl(uint32_t host32bitvalue);
  4. //均返回:网络字节序的值
  5. uint16_t ntohs(uint16_t net16bitvalue);
  6. uint32_t ntohl(uint32_t net32bitvalue);
  7. //均返回:主机字节序的值
上面函数的名字中,h 代表 host,n 代表 network,s 代表 short,l 代表 long。现在我们通常把 s 视为一个16位的值(例如TCP或UDP端口号),把 l 视为一个32位的值(例如IPv4地址)。

四、值-结果参数

当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针。该结构的长度也作为一个参数来传递,不过其传递的方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程。

1. 从进程到内核传递套接字地址结构的函数有3个:bind、connect 和 sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
这样做的原因是,既然指针和指针所指内容的大小都传给了内核,于是内核就知道了到底需要从进程复制多少数据进来。

2. 从内核到进程传递套接字地址结构的函数有4个:accept、recvfrom、getsockname 和 getpeername。(未介绍的函数参见《Unix 网络编程》)这四个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
这里与上面不同的是,把套接字地址结构大小这个参数从一个整数改为指向某个整数变量的指针,其原因在于:当函数被调用时,结构大小是一个值,它告诉内核该结构的大小,这样内核在写该结构时不至于越界; 当函数返回时,结构大小又是一个结果,它告诉进程内核在该结构中究竟存储了信息。这种类型的参数称为值-结果参数。


参考资料:《Unix 网络编程》

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/158772
推荐阅读
相关标签
  

闽ICP备14008679号