UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1

时间:2022-11-07 11:00:50

还是那句话,我们要学会使用man查看

1.socket函数

根据指定的协议族、套接字类型和协议来分配一个套接口的描述符及其所用的资源,返回分配的套接字描述符

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

此处需要重点理解socket每个参数的含义:

  • domain:指定协议族,我们可以通过man来查看不同类unix操作系统提供的常量值。在我的ubuntu14.04中

UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1

我们常用的AF_UNIX(Unix域协议),AF_INET(ipv4协议),AF_INET6(ipv6协议)

  • type:制定套接字类型
    • SOCK_STREAM :字节流套接字
    • SOCK_DGRAM:数据报套接字
    • SOCK_SEQPACKET:有序分组套接字
    • SOCK_RAW:原始套接字

UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1

  • protocol:使用的传输层协议
    • IPPROTO_TCP: tcp传输协议
    • IPPROTO_UDP:udp传输协议
    • IPPROTO_SCTP:SCTP传输协议

*****************socket在网络编程中的调用位置******************************************

UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1


2.connect函数

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

  • sockfd:传入由socket函数返回的套接字描述符。
  • addr:指向套接字地址结构的指针
  • 套接字地址结构的大小

知识点总结:

  • 如果是tcp套接字,connect函数激发tcp的三路握手过程
  • 查看connect函数返回结果。若返回成功,则tcp连接建立成功,客户端tcp状态从SYN_SENT -> CONNECTED
  • connect出错返回的几种情况
    • 情况1:若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。(超时错误)
    • 情况2:若对客户的SYN的响应是RST(复位),表明该服务器主机在我们指定的端口上没有进程在等待与之连接(比如:服务器进程也许没在运行)。这是硬件错误,客户端接收到RST就马上返回ECONNREFUSED。
    • 情况3:若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable"(目的地不可达)ICMP错误,则认为是一个软错误。客户主机内核保存该信息,并按重传机制间隔继续发送SYN。若在某个规定的时间后仍未收到响应,则把保存的消息(即ICMP)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。以下情况也是可能的:一是按照本地系统的路由表,根本没有到达远程系统的路径。二是connect调用根本不等待直接返回。

    RST解析:

  • RST是TCP在发生错误时发送的一种TCP分节(RST本身也是一个分节)
  • 产生RST的三个条件:(记清楚了)目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;TCP想取消一个已有连接;TCP接收到一个根本不存在的连接上的分节

调试验证(纸上得来终觉浅,绝知此事要躬行)

运行书中时间服务程序 intro目录下

 sudo ./daytimetcpsrv
另开一个终端,使用netstat命令查看服务是否成功开启,结果如下

sunxiaowu@sunxiaowu:~$ sudo netstat -anp | grep 'daytime'
tcp 0 0 0.0.0.0:13 0.0.0.0:* LISTEN 2714/daytimetcpsrv
sunxiaowu@sunxiaowu:~$
运行客户端时间获取程序

sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$ ./daytimetcpcli 127.0.0.1
Fri Jul 7 10:53:55 2017
sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$
成功获得服务器时间

现在我们来模拟connect出错情况1

我们尝试指定主机不存在的一个IP地址,这样当客户主机发送请求(要求那个不存在的主机响应以其硬件地址)时,它将永远收不到ARP响应(这个ARP响应是什么,以后详解,可以参看tcp/ip详解),收不到SYN分节的响应。

sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$ ./daytimetcpcli 192.122.2.2
connect error: Connection timed out
sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$
这个等待的具体时间可能有点长(中间会涉及tcp的重传机制),注意看报错的函数,正是connect

现在我们来模拟connect出错情况2

典型情况是可以通过ip找到主机,也就是主机会有回应,但客户端请求的服务端口并没有进程等待与之连接。我们可以把时间服务程序关掉,直接运行客户端时间获取程序来模拟此种错误。

sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$ sudo netstat -anp |grep 'daytime' 
[sudo] password for sunxiaowu:
tcp 0 0 0.0.0.0:13 0.0.0.0:* LISTEN 2714/daytimetcpsrv
sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$ sudo kill 2714
sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$ sudo netstat -anp |grep 'daytime'sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$ ./daytimetcpcli 127.0.0.1
connect error: Connection refused
sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$
注意看报错,主机会响应一个RST分节,导致connect报错,报错信息connection refused

现在我们来模拟connect出错情况3

我们指定一个不可到达的ip地址。

sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$ ./daytimetcpcli 192.168.1.33
connect error: No route to host
sunxiaowu@sunxiaowu:~/Downloads/unpv13e/intro$

connect总结:

结合tcp状态图(可以查看我的另一篇博文:TCP状态转换详解),

  • 主动连接方(一般就是客户端)应用进程调用connect时,会发送SYN分节给服务端,状态CLOSED ->  SYN_SENT,接收到对方的ACK和SYN分节后,connect成功返回,状态

SYN_SENT -> CONNECTED

  • 若connect失败,则该套接字不能再使用,必须关闭,我们不能对这个套接字再次调用connect。(以前挂过彩,恍如昨日)


3.bind函数

把一个本地协议地址赋予一个套接字。

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

此函数直接查看man手册学习吧

注意一点,bind返回的而一个常见错误是EADDRINUSE(地址已使用)

4.listen函数

  #include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);

  • 调用listen函数导致套接字从CLOSED -> LISTEN
  • listen把未连接的套接字(默认主动)转换成一个被动套接字(也就是说,它指示内核应接收指向该套接字的请求)
  • ****第二个参数规定内核应该为相应套接字排队的最大连接数

重点:内核会为任何一个给定的监听套接字维护两个队列:

UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1

这个在面试中也是被人家常问的(内核具体怎么维护这两个队列,以后在内核解析中慢慢讲述)

当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节。这一项一直保留在未完成队列中,直到三路握手的第三个分节到达或者该项超时为止。若三路握手正常完成,该项就从未完成连接队列转移到已完成连接队列的队尾。当进程调用accept,已完成连接队列中的对头项将返回给进程。如果队列为空,则进程将被投入睡眠,直到tcp在该队列中放入一项才唤醒它。

UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1

如何获取系统支持的最大LISTENQ?

NAME
getenv, secure_getenv - get an environment variable

SYNOPSIS
#include <stdlib.h>
char *getenv(const char *name);
char *secure_getenv(const char *name);

重点:

  • 当一个客户SYN到达时,若这些队列是满的,TCP就忽略该TCP分节,也就是不发送RST。客户TCP将重发SYN,期望不久能在队列中找到可用空间。

  • 在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器tcp排队,最大数据量为相应已连接套接字的接收缓冲区大小。

5.accept函数

#include<sys/socket.h>

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

返回值:若成功则为非负描述符,若出错则为-1

  • 如果accept成功,那么其返回值为由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。
  • accept中的第一个参数为监听套接字描述符,返回值为已连接套接字描述符

6.fork函数

 #include <unistd.h>
pid_t fork(void);
返回:在子进程中为0,父进程中为子进程ID,若出错则为-1

  • fork调用一次,返回2次
  • 调用进程返回一次,返回值为新派生进程的pid,子进程返回一次,返回值为0
  • 父进程调用fork之前打开的所有描述符在fork返回之后由子进程分享。

fork两种典型用法:

  • 一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的操作
  • 一个进程想要执行另一个程序。fork,然后其中一个副本调用exec族函数(把当前进程映像替换成新的程序文件,从main开始执行,进程id不变。

7.exec族函数


8.close函数

  #include <unistd.h>
int close(int fd);
用来关闭套接字,并终止tcp连接

重点:close一个tcp套接字的默认行为是把该套接字标记为已关闭(导致相应描述符的引用计数值减1),然后立即返回到调用进程。该套接字描述符不能再由调用进程使用(不能再作为read或write的第一个参数)

注意,此时tcp将尝试发送已排队等待的到对端的任何数据(发送队列),发送完毕后发生的是正常的tcp连接终止序列(计数为0才会引发四分组连接终止)。


9.shutdown函数

#include<sys/socket.h>
int shutdown(int sockfd,int howto);
若成功则为0,出错返回-1

终止网络的通常方法:调用close函数。不过close有两个限制,却可以使用shutdown避免

  • close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。而使用shutdown可以直接触发tcp的正常连接终止序列。
  • close终止读和写两个方向数据的传送(close某个套接字后,此套接字描述符不能再做为read或write的第一个参数),而shutdown行为依赖于howto
  • howto参数详解:

UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1

UNIX网络编程卷1:套接字联网-第4章:基本TCP套接字编程1