第4章 基本tcp套接字编程

Posted perfy576

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第4章 基本tcp套接字编程相关的知识,希望对你有一定的参考价值。

4.1 各种套接字api(重要

4.1.1 socket()

用于创建一个套接字描述符,这个描述符指明的是tcp还是udp,同时还有ipv4还是ipv6

#include <sys/socket.h>
?
int socket(int family, int type, int protocol);
//成功返回描述符,错误-1
  • family主要是指明的协议族,AF_INET:ipv4、AF_INET6:ipv6 、AF_LOCAL:unix域协议、AF_ROUTE:路由套接字、AF_KEY秘钥套接字

    网络编程中主要还是前两种

  • type指明套接字类型,主要是数据报,还是流式,原始套接字

    SOCK_STREAM:流式,SOCK_DGRAM:报式 、SOCK_SEQPACKET有序分组套接字、SOCK_RAW原始套接字

  • protocol是控制协议,通常位0,表示由前两个参数组合出来的协议的默认类型

 AF_INETAF_INET6AF_LOCALAF_ROUTEAF_KEY
SOCK_STREAM TCP|SCTP TCP|SCTP    
SOCK_DGRAM UDP UDP    
SOCK_SEQPACKET SCTP SCTP    
SOCK_RAW ipv4自己填写 ipv6自己填写  

 

其中AF_PF_开头的都有,但是前一个表示地址族,后一个表示协议族。但是后一个现在很少用。

 

socket()创建的是主动套接字。现在获得的套接字还不能够像普通的文件描述符一样进行读写,套接字描述符需要绑定本端套接字(bind)和对端套接字(connect,或是send)。

 

4.1.2 connect()

将一个套接字描述符与一对套接字地址绑定。这样就使得套接字像是一个打开文件获得的文件描述符一样,可以通过操作这个描述符来操作与一个地址之间的通信数据。


#include <sys/socket.h>
?
int connect(int sfd, const struct sockaddr *sevaddr, socklen_t addrlen);
//成功0,出错-1,设置errno

 

  • sfd是由socket()函数获得的套接字

  • sockaddr *sevaddr可以看出来,传入的参数只能是sockaddr类型的,所以需要强转

    另外通过名字可以知道,绑定的是一个服务器地址。

  • addrlen套接字地址结构的长度,有了这个长度,内核才知道,要复制多少数据。

4.1.2.1 阻塞套接字connect

connect()通常都是客户端调用去连接服务器,调用connect()以后,本机就与sevaddr指定的主机进行连接。如果是tcp那么会触发三次握手, 当套接字是阻塞套接字时,该函数仅仅在出错或是建立成功以后才会返回,出错的情况有:

  • 直接死在arp上,返回-1,errno设置ETIMEOUT,或是死在路由器的arp上是UN**REACH

  • 本机发送syn,且重发,并等待总共75秒后,没有收到syn分节的回应。ETIMEDOUT

  • 本机发送syn分节,收到rst复位分节,表示在服务器的指定端口上,没有进程在等待连接。这是一种 硬错误,也就是不是重试能够解决的。函数返回ECONNREFUSED

    能够收到rst分节的情况有(这是拓展)

    • 对应服务器的端口上,没有进程在等待连接,也就是没有listen()

    • tcp想要终止一条连接,本端EPOLLERR

    • tcp收到了一条不存在连接上的数据,也就是,收到一条陌生的数据,而且该数据不是syn分节

  • 本机发送syn分节,但是在syn分节在到服务器的中途中的某个 路由器上引发一个目的不可达的icmp错误,是一种软错误,可以通过重发解决。本机接收到icmp报文以后,重试,如果在75秒内没有收到syn分节则返回EHOSTUNREACH或是ENETUNREACH错误

    注意这里仅仅只路由器死在arp的时候返回的icmp会这样处理,直接交付数据报死在arp上,是ETIMEDOUT

    目的不可达的原因(代码默认ipv4):

    • 主机不可达1,是由路由器或是本机,当本机要求直接交付数据(子网),但是该主机已经离线,死在arp上,(这样貌似是本机产生icmp),或是路由器也死在arp上,那么就会发送icmp。

      其中icmpv6中,将直接交付产生的目的不可达,单独作为一个代码,0

    • 禁止通信3,通常由路由器丢弃流量导致,通常情况下,不会产生这类报文,防火墙直接丢弃,不产生。

    • 端口不可达3,通常是udp中,数据包的目的端口,没有进程在监听。返回端口不可达的icmp

      代码和禁止通信一样

    • 数据报大于MTU但是设置部分片4,产生目的不可达icmp

    这种情况下,该函数会:

    • 直接返回

    • ???

    但是ENETUNREACH不可达已经过时了,应该将两种错误看作一种处理。

当connect失败的时候,必须关闭套接字,不能再次对同一个套接字进行connect**

4.1.2.2 非阻塞套接字connect

非阻塞connect套接字的作用:

  • 完成一个connect要花费RTT时间,而RTT波动范围很大,从局域网上的几个毫秒甚至是广域网上的几秒,这段时间也许有我们要执行的其他处理工作可以执行。

  • 可以使用这个技术同时建立多个连接。

  • 许多connect的超时实现以75秒为默认值,如果应用程序想自定义一个超时时间,就是使用非阻塞的connect.

在一个非阻塞的套接字上调用connect()connect()会立即返回EINPROGRESS错误(非本机),0(本机),但是已经发起的TCP三次握手继续进行。

通常,非阻塞的套接字,我们不会直接去处理connect()后的套接字,而是在connect()后,将该套接字,放入IO复用的api中。

非阻塞connect套接字实现时需要注意的细节:

  • 连接到同一主机上,connect会立即完成,我们必须处理这种情形

    调用connect,如果返回0,表示连接已经完成,如果返回-1,那么期望收到的错误errnoEINPROGRESS,连接建立已经 启动,但是尚未完成。

  • POSIX关于select和非阻塞connect的以下两个规则:

    • 连接成功,描述符会变成可写 (连接建立时,写缓冲区空闲,所以可写)

    • 连接建立遇到错误时,描述符变为可读可写(由于有未决的错误,从而可读又可写)通常是EPOLLERR带上之前监听的事件,严格上如果只监听了读,那么就没有

完整的io复用流程位:

参考

  1. 创建非阻塞套接字,或是,创建以后调用fcntl把套接字设置为非阻塞

  2. 调用connect,如果返回0,表示连接已经完成,如果返回-1,那么期望收到的错误是EINPROGRESS,连接建立已经 启动,但是尚未完成。

  3. 调用select,并且设置超时时间

  4. 超时处理

    如果select返回0,超时发生,那么返回ETIMEOUT错误给调用者,并且关闭套接字,防止已经启动的三路握手继续下去。

  5. 连接错误和成功处理

    如果描述符变为可读或可写,通过getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len)获取的error 值,如果建立连接时遇到错误,则errno 的值是连接错误所对应的errno值,比如ECONNREFUSEDETIMEDOUT连接成功: getsockopt返回0 连接失败:getsockopt返回0, 并且获取相应的错误。

    muduo使用的是该方法,直接监听读事件,当由EPOLLERR事件的时候,直接关闭.

    另一种方法是参考

    Linux环境下是有效的:

    再次调用connect,相应返回失败,如果错误errno是EISCONN,表示socket连接已经建立,否则认为连接失败。

4.1.2.3 udp的connect()

默认udp一个套接字是无连接的,可以向多个地址send数据,但是一旦connect()就只能向一个地址发送数据了。

4.1.2.4 常见错误码

EACCES, EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。

EADDRINUSE98:Address already in use(本地地址处于使用状态)

EAFNOSUPPORT97:Address family not supported by protocol(参数serv_add中的地址非合法地址)

EAGAIN:没有足够空闲的本地端口。

EALREADY114:Operation already in progress(套接字为非阻塞套接字,并且原来的连接请求还未完成)

EBADF77:File descriptor in bad state(非法的文件描述符)

ECONNREFUSED 111:Connection refused(远程地址并没有处于监听状态)

EFAULT:指向套接字结构体的地址非法。

EINPROGRESS 115:Operation now in progress(套接字为非阻塞套接字,且连接请求没有立即完成)

EINTR:系统调用的执行由于捕获中断而中止。

EISCONN 106:Transport endpoint is already connected(已经连接到该套接字)

ENETUNREACH 101:Network is unreachable(网络不可到达)

ENOTSOCK 88:Socket operation on non-socket(文件描述符不与套接字相关)

ETIMEDOUT 110:Connection timed out(连接超时)

 

4.1.3 bind()

常用用于服务器绑定ip和端口,进行监听

一个tcp连接,需要一对套接字地址结构:对端和本端。在上面的connect中,我们是客户端想服务端主动发起一个连接请求,指定了对端的套接字结构,但是没有指定本端的,此时内核会为我们随机选取端口号(当然有范围要求)和ip地址(当本端有多个网卡时)。也就是说本端套接字地址结构是随机的。

但是在服务器端,服务器本端的套接字地址结构需要固定,不然客户端怎么连接。因此服务器需要显式显式的指明套接字描述符的本端套接字地址结构。

bind()函数的主要作用是,为套接字描述符绑定本端的套接字地址结构,也就是绑定ip地址和端口。


#incldue <sys/socket.h>
int bind(int sockfd, const struct sockaddr *selfaddr, socklen_t len);
//成功0,错误-1

 

  • sockfd套接字描述符

  • sockaddr *selfad需要强转,这个套接字地址结构,通常是绑定的本端的信息。

    客户端连接本端时候使用的ip,客户端连接本端使用的端口,也就是connect()中的套接字结构信息是一样的。

  • len内核复制数据需要的长度。

sockaddr *selfad各种情况:

  • 端口

    是一个需要让别人知道的端口。如果不指定,那么内核随机选,那么你还绑定干啥?

  • ip地址

    ip地址必须是本机的地址(当存在多个网卡,多个ip地址的时候需要绑定),一旦绑定了,那么只有connect()填写的ip地址是这个ip地址的时候,数据才能被接受,不是的就丢弃了。

    当该ip不填,是0的时候,那么所有发送到本机的数据包都能被接受。后续的链接如果不设设置REUSEADDR那么不能绑定成功.

通配地址

当不显示指明ip地址的时候,一般需要下面的宏,和变量

//ipv4
seraddr.sin_addr.s_addr=htonl(INADDR_ANY);
//ipv6
#include <netinet/in.h>
?
extern in6addr_any;
seraddr.sin6_addr=in6addr_any;

 

这里使用htonl()原因在,在套接字地址结构中的数据都是网络字节序的。

bind()通常的错误是EADDRINUSE表示本端的这个套接字已经在使用了,一般是地址.端口不能重用

4.1.4 listen()

上面bind了套接字地址结构以后,还没有开始监听啊

#include <sys/socket.h>
?
int listen(int sfd, int backlog);
//成功0,错误-1

 

 

 

listen()的主要作用:

  • 将主动套接字转化为被动套接字

    套接字的状态从closedlisten

  • 规定套接字排队的最大连接个数

    • 已完成连接队列

      当次队列为空的时候,accept()休眠

    • 未完成连接队列

      正在进行三次握手的连接

 

其实这里还有另一层意思,当我们给以套接字描述符绑定本端的时候,意味着我们可以读取这个文件描述符。当对端发来数据以后,我们从listen的文件描述符中读取已经连接的文件描述符。监听套接字是一个只读套接字

4.1.5 accept()

该函数的作用是返回已完成连接队列的第一个连接的套接字描述符,如果队列为空,那么该函数将被阻塞(如果是阻塞套接字的话)。

#include <sys/socket.h>
int accpet(int sfd, struct sockaddr *cliaddr, socklen_t *addrlen);
//成功描述符,错误-1

 

  • cliaddr是将要被内核填充的对端的套接字地址结构。addrlen也是内核要填充的地址结构的长度。

    这两个参数都可以为NULL

如果函数成功返回,那么返回的是一个套接字描述符,这个描述符关联了一对套接字地址结构,因此可以当做普通文件描述符来使用。

accpet()涉及到两个描述符,一个是sfd称为监听描述符,而函数返回的描述符是已连接套接字描述符,我们使用这个文件描述符与对端进行数据交换。

 

4.1.6 close()


#include <unistd.h>
int close(int fd);
//成功0,错误-1

 

这个函数的功能是,将文件描述符的引用计数-1,当为0的时候,则关闭文件描述符,tcp情况下触发四次挥手操作,或是rst。

accept返回的文件描述符应该及时关闭。

4.1.7 get××name()

获取一个套接字描述符绑定一对套接字地址结构


#include <sys/socket.h>
int getsockname(int sfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sfd, struct sockaddr *peeraddr, socklen_t *addrlen);
//成功0,错误-1

 

这两个函数是用来让返回一个套接字描述符绑定的两个套接字地址结构。

我们可以使用这两个函数:

  • getsockname获取内核为我们选去的ip地址和端口号、协议族

  • getpeername可以返回对端的套接字地址结构(和accpet填充的一样?)

  • getpeername是唯一能够在accept以后又调用exec函数后获得对端套接字地址结构的函数。

    返回的已完成连接套接字文件描述符不是O_EXECLOSE,因此在exec以后仍然打开,同时,exec以后,所有的地址信息不能用了,因此accept的就不能用了。所以需要getpeername返回对端套接字地址结构。

    然后,通过在exec的时候传入文件描述符,或是在exec之前将文件描述符更改为exec要执行程序默认的一个文件描述符。传递参数(inetd采用第二种)。

4.1.8 shutdown()

close终止两个方向上的数据传输


#include <sys/socket.h>
int shutdown(int sockfd, int howto);
//成功0,错误-1

 

  • howto表示行为:

    • SHUT_RD关闭连接的读,也就是套接字本端不再接受数据,缓冲区现有数据被丢弃

      也不能再使用读函数对套接字进行操作,对TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。

      会超时然后EPOLLERR?应为不会发送FIN.

    • SHUT_WR 关闭连接的写,也就是本端不再写数据,缓冲区中现有数据将被发送,然后发送FIN分节。

      EPOLLIN事件

    • SHUT_RDWR 第一次调用SHUT_RD,然后再调用SHUT_WR

  • sockfd文件描述符

使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的参考数为0时才关闭连接。

shutdown可直接关闭描述符,不考虑描述符的参考数,可选择中止一个方向的连接。

 

以上是关于第4章 基本tcp套接字编程的主要内容,如果未能解决你的问题,请参考以下文章

Windows程序设计笔记4:第10章:TCP/IP和网络通信

C/C++ 网络编程4: 基本TCP套接字编程

Python 之 Socket编程(TCP/UDP)

基本TCP套接字编程

第13章 TCP编程_TCP的连接和关闭过程

第三模块:面向对象&网络编程基础 第2章 网络编程