网络编程Socket通信之TCP

先了解一下关于TCP的相关知识。

  • 1.1   TCP和UDP

1.TCP面向连接;UDP是无连接

  每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。

2.TCP可靠、有序;UDP不可靠、无序

3.TCP字节流模式--SOCK_STREAM;UDP数据报模式--SOCK_DGRAM

TCP发送报文和接收报文的次数无需一一对应;UDP发送报文和接收报文的次数需要一一对应。

4. TCP一般应用在对可靠性要求比较高的场合,例如ftp文件传输、电子邮件等等,UDP一般应用在对实时性要求较高场合,例如视频直播。

  • 1.2 报文格式

  • 1.3 主机序与网络序

1.主机序

      不同的CPU有不同的字节序类型这些字节序是指整数在内存中保存的顺序,这个叫做主机序。

a.Little endian:将低序字节存储在起始地址。 即小端模式

b.Big endian:将高序字节存储在起始地址。  即大端模式

 

2.网络序

  网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式,即先发送高字节数据再发送低字节数据。

  TCP网络中传输数据采用网络序。在进行网络编程发送数据时,需要通过hton系列接口转成网络序;在从网络中接收到数据后通过ntoh系列接口转成主机序。

 

然后了解一下TCP通信模型。

  • 2.1 TCP建立连接

 

    TCP建立连接的三次握手与四次挥手。

 

 

一、time_wait状态是什么?

 

  time_wait状态存在于client收到server Fin并返回ack包时的状态。当处于time_wait状态时,由于port被占用,我们无法在用此端口创建新的连接。

 

二、time_wait存在的意义?

 

  1.可靠地实现TCP全双工连接的终止。为了保证A发送的最后一个ACK报文能够到达B。A发送的这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。B会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段。如果A在TIME-WAIT状态不等待一段时间,而是在发送完ACK报文段后就立即释放连接,就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法按照正常的步骤进入CLOSED状态。

 

  2.保证让迟来的TCP报文段有足够的时间被识别并丢弃。A在发送完ACK报文段后,再经过2MSL时间,就可以使本连接持续的时间所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求的报文段。

 

三、哪一方会有time_wait状态

 

  time_wait状态是一般有client的状态,并且会占用port,有时产生在server端,由于server主动断开连接或者发生异常。

 

四、time_wait状态过多会有什么问题?如何解决?

 

  由于time_wait状态会占用端口资源(Linux默认是60s,windows默认是30s连接消失),如果time_wait过多后会导致端口资源耗尽而无法建立新的连接。

 

解决办法有:

 

  1.修改TCP参数配置,具体网上查询。

 

  2.服务端listen的socket要设置地址复用用于服务重启时侦听端口可以立即复用,accept出来的套接字不用设置地址复用。

  3.客户端断开连接时,调用lingeron接口来让客户端快速断开连接。

  • 2.2 通信一般流程

 

 最后,编程时的函数。

2.2.1 创建socket套接字

函数原型:

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

功能介绍:

  在Linux操作系统中,一切皆文件,这个大家都知道,个人理解创建socket的过程其实就是一个获得文件描述符的过程,当然这个过程会是比较复杂的。可以从内核中找到创建socket的代码,并且socket的创建和其他的listen,bind等操作分离开来。socket函数完成正确的操作是返回值大于0的文件描述符,当返回小于0的值时,操作错误。同样是返回一个文件描述符,但是会因为三个参数组合不同,对于数据具体的工作流程不同,对于应用层编程来说,这些也是不可见的。

参数说明:

  从socket创建的函数可以看出,socket有三个参数,family代表一个协议族,比较熟知的就是AF_INET,PF_PACKET等;第二个参数是协议类型,常见类型是SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_PACKET等;第三个参数是具体的协议,对于标准套接字来说,其值是0,对于原始套接字来说就是具体的协议值。

2.2.2 地址端口绑定函数bind

函数原型:

  int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

功能介绍:

  bind函数主要应用于服务器模式一端,其主要的功能是将addrlen长度 struct sockaddr类型的myaddr地址与sockfd文件描述符绑定到一起,在sockaddr中主要包含服务器端的协议族类型,网络地址和端口号等。在客户端模式中不需要使用bind函数。当bind函数返回0时,为正确绑定,返回-1,则为绑定失败。

参数说明:

  i.bind函数的第一个参数sockfd是在创建socket套接字时返回的文件描述符。

  ii.bind函数的第二个参数是struct sockaddr类型的数据结构,由于struct sockaddr数据结构类型不方便设置,所以通常会通过对struct sockaddr_in进行地质结构设置,然后进行强制类型转换成struct sockaddr类型的数据,下面是两种类型数据结构的定义和对应关系图。

  iii.bind函数的第三个参数是指定struct sockaddr类型数据的长度,因为前面讲过bind函数的第二个参数是通过设置一个较容易的数据结构,然后通过强制类型转换成struct sockaddr,实际上,第二个参数具体的数据结构的长度会根据socket创建时,设置的family协议族的不同而不同,像AF_UNIX协议族的bind函数第二个参数的数据结构应该是struct sockaddr_un,其大小和struct sockaddr_in不同。

2.2.3 监听本地端口listen

函数原型:

  int listen(int sockfd, int backlog);

功能介绍:

  刚开始理解listen函数会有一个误区,就是认为其操作是在等在一个新的connect的到来,其实不是这样的,真正等待connect的是accept操作。listen的操作就是当有较多的client发起connect时,server端不能及时的处理已经建立的连接,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度有listen中的backlog参数来设定。listen和accept函数是服务器模式特有的函数,客户端不需要这个函数。当listen运行成功时,返回0;运行失败时,返回值位-1。

参数说明:

  sockfd是前面socket创建的文件描述符;backlog是指server端可以缓存连接的最大个数,也就是等待队列的长度。

2.2.4 接受网络请求函数accept

函数原型:

  int accept(int sockfd, struct sockaddr *client_addr, socklen_t *len);

功能介绍:

  接受函数accept其实并不是真正的接受,而是客户端向服务器端监听端口发起的连接。对于TCP来说,accept从阻塞状态返回的时候,已经完成了三次握手的操作。Accept其实是取了一个已经处于connected状态的连接,然后把对方的协议族,网络地址以及端口都存在了client_addr中,返回一个用于操作的新的文件描述符,该文件描述符表示客户端与服务器端的连接,通过对该文件描述符操作,可以向client端发送和接收数据。同时之前socket创建的sockfd,则继续监听有没有新的连接到达本地端口。返回大于0的文件描述符则表示accept成功,否则失败。

参数说明:

  sockfd是之前socket创建的文件描述符;client_addr是本地服务器端的一个struct sockaddr类型的变量,用于存放新连接的协议族,网络地址以及端口号等;第三个参数len是第二个参数所指内容的长度,对于TCP来说其值可以用sizeof(struct sockaddr_in)来计算大小,要说明的是accept的第三个参数要是指针的形式,因为这个值是要传给协议栈使用的。

2.2.5 连接目标服务器函数connect

函数原型:

  int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);

功能介绍:

  连接函数connect是属于client端的操作函数,其目的是向服务器端发送连接请求,这也是从客户端发起TCP三次握手请求的开始,服务器端的协议族,网络地址以及端口都会填充到connect函数的serv_addr地址当中。当connect返回0时说明已经connect成功,返回值是-1时,表示connect失败。

参数说明:

  connect的第一个参数是socket创建的文件描述符;第二个参数是一个struct sockaddr类型的指针,这个参数中设置的是要连接的目标服务器的协议族,网络地址以及端口号;第三个参数表示第二个参数内容的大小,与accept不同,这个值不是一个指针。

  在服务器端和客户端建立连接之后是进行数据间的发送和接收,主要使用的接收函数是recv和read,发送函数是send和write。因为对于socket套接字来说,最终实际操作的是文件描述符,所以可以使用对文件进行操作的接收和发送函数对socket套接字进行操作。对于UDP编程来说,其服务器端和客户端之间没有三次握手建立连接,所以服务器端没有listen和accept函数,客户端没有connect函数。所以对于服务器端来说,没有accept函数,所以使用recvfrom函数来获取数据的同时获得客户端的协议族,网络地址以及端口号;对于客户端来说,没有connect函数,所以使用sendto函数发送数据的同时设置服务器端的协议族,网络地址以及端口;同理如果recvfrom用在客户端,则是接收服务器端数据和地址,sendto用在服务器端,则是发送到客户端网络地址以及端口数据。

2.2.6 接收数据函数recv

函数原型:

  int recv( int sockfd, char FAR *buf, int len, int flags);

功能介绍:

  对于该函数主要的功能是,从客户端或者服务器端接收数据,如果函数返回-1,所说明接收数据失败,如果返回的是大于等于0的值,则说明函数接收到的数据的大小。因为可以设置文件描述符的状态为阻塞模式,所以在没有接收到数据时,recv会一直处于阻塞状态,直到有数据接收到。

参数说明:

  sockfd是建立连接时的文件描述符;buf用于存储接收到的数据缓冲区,接收的数据将放到这个指针所指向的内容的空间中;len是接收缓冲区的大小;flags一般置为0。

2.2.7 发送数据函数send

函数原型:

  int  send(int sockfd, const char FAR *buf, int len, int flags);

功能介绍:

  send函数主要向客户端或者服务器端发送数据,当返回值-1时,表明发送失败,当返回值大于等于0时,表示发送成功,并且发送数据的大小会通过返回值传递回来。

参数说明:

  sockfd是有socket创建或建立连接时的文件描述符;buf是发送数据缓冲区,要发送的数据会放在这个指针指向的内容空间中;len是发送缓冲区的大小;flags一般置为0。

Socket参数--SO_REUSERADDR

  i.允许一个监听服务器bind到一个端口,即使之前服务端口的连接存在。

  ii.允许同一个端口上启动同一个服务器的多个实例,每一个实例绑定的IP地址不能相同。

  iii.允许单个进程绑定同一个端口到多个socket,但每一个socket绑定的IP地址不能相同。

  iv.允许完全相同的IP地址和端口的重复绑定,只能用于UDP的多播

 

posted @ 2018-11-10 15:06  泪落凝萌  阅读(402)  评论(0编辑  收藏  举报