基本TCP套接字编程

时间:2021-03-12 10:22:11

基本TCP套接字编程

tcp提供了可靠传输,当tcp向另一端发送数据的时候,要求对端返回一个确认。如果没有接收到确认,tcp就重传数据并且等待更长时间,数次重传失败后,tcp才放弃。

建立一个tcp连接会发生如下事情:

  1. 服务器必须准备好接受外来的连接请求,通常通过socket,bind,listen这三个函数完成。
  2. 客户端通过connect连接服务器,主动打开,导致tcp客户端发送一个SYN字节,通常SYN不携带数据。
  3. 服务端必须确认客户的SYN字节,即ACK字节,同时服务端还会发送一个SYN字节给客户端
  4. 客户端必须确认服务端的SYN。

每一个SYN可以包含多个TCP选项:

  • MSS选项:向对端通告最大分节大小,即愿意接收的最大数据量。
  • 窗口规模选项:TCP首部中接收窗口首部为16位,意味着最大接收窗口为65535.
  • 时间戳选项:对于高速网络是必要的,可以防止失而复现的分组可能造成的数据损坏。

相关函数:

(1)socket()

#include <sys/socket.h>
int socket(int family, int type, int protocol);
  • family:指定协议族,也称为协议域。取值为下面中的一个

基本TCP套接字编程

  • type,指明套接字类型

基本TCP套接字编程

  • protocol:协议类型常值,设为0的话表示选择所给定的family和type组合的系统默认值。

(2)connect()

#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr* servaddr, socklen_t addrlen);
成功返回0,否则返回-1
  • sockfd:是由socket返回的套接字。
  • servaddr:包含服务器IP地址和端口号的套接字地址结构
  • addrlen:套接字地址结构的大小。

客户在调用connect之前不必一定需要调用bind函数,内核会确定源IP并选择一个临时端口作为源端口。如果是TCP套接字,调用connect将激发TCP三次握手过程。函数会阻塞进程,直到成功返回或者出错。

  1. 返回ETIMEDOUT错误:内核发送一个SYN,如没有相应,则等待6s后在发送一个,若仍无相应,则等待24秒再发送,总共等待75s后仍没有响应,则返回该错误。
  2. 返回ECONNREFUSED:服务器对客户端响应一个RST,表明服务器在客户指定的端口上没有进程在等待与之连接。产生RST还有两个条件:1)TCP想取消一个已有连接;2)TCP接收到一个根本不存在的连接上的分节
  3. 返回EHOSTUNREACH或ENETUNREACH错误:客户发出的SYN在中间的某个路由引发一个“目的地 不可达”ICMP错误,内核会报错该消息,并按情况1中的间隔继续发送SYN,若在规定时间内仍未收到响应,则把报错的信息作为这两种错误之一返回给进程。

connect若失败,则该套接字不可用,则必须先关闭该套接字,然后再次调用connect函数。

(3)bind函数:把一个本地协议地址赋予一个套接字

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

调用bind可以绑定IP地址或者端口,可以两者都指定,也可以两者都不指定。

  • 如果指定端口为0,那么内核在bind被调用得时候选择一个临时端口。
  • 如果指定IP地址为通配地址(对IPv4来说,通配地址由常值INADDR_ANY来指定,值一般为0),内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址

如果让内核来为套接字选择一个临时端口号,函数bind并不会返回所选择的值。第二个参数有const限定词,它无法返回所选的值。如果想得到内核所选择的临时端口值,必须调用getsockname函数

(4)listen函数

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

listen做两件事:

  1. 当socket创建一个套接字的时候,它被设定为一个主动套接字。listen将其转成一个被动套接字,指示内核应接受指向该套接字的连接请求
  2. 第二个函数规定了内核应该为相应套接字排队的最大连接个数。

内核为任一给定的监听套接字维护两个队列,两个队列之和不超过backlog

  • 未完成队列
  • 已完成队列

基本TCP套接字编程

当进程调用accept函数的时候,如果已连接队列不为空,那么队头项将返回给进程。否则进程进入睡眠,直到TCP在该队列中放入一项才唤醒它;不要把backlog设置为0;设置backlog的一种方法是使用一个常值。SYN到达的时候,如果队列已满,TCP忽略该SYN分节。

(5)accept()

#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t addrlen);
  • sockfd:监听套接字描述符
  • cliaddr:已连接的对端客户的套接字地址结构
  • addrlen:调用时指示内核向cliaddr写入的最大字节数。

如果对返回客户协议地址不感兴趣,可以把cliaddr和addrlen均置为空指针。

(6)close()

#include<unistd.h>
int close(int sockfd);

close一个TCP套接字的默认行为是套接字标记关闭,立即返回调用进程。然后TCP将尝试发送已排队等待发送到对端的任何数据。close会将套接字描述符的引用计数减1,如果引用计数仍大于0,则不会引起TCP的四次挥手终止序列。

(7)shutdown()

#include<sys/socket.h>
int shutdown(int sockfd, int howto);

  • howto
    • SHUT_RD:关闭连接的读这一半,套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数(对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃)
    • SHUT_WR:关闭连接的写这一半对于TCP,称为半关闭),套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。进程不能再对这样的套接字调用任何写函数
    • SHUT_RDWR:连接的读半部和写半部都关闭。等价于调用2次shutdown,分别指定SHUT_RD与SHUT_WR

shutdown和close的区别:

  • 关闭套接字的时机不同:close把描述符的引用计数减一,仅在引用计数为0的时候,才会关闭套接字,而shutdown可直接关闭套接字
  • 全关闭和半关闭:close终止读和写两个方向的数据传送。shutdown可以关闭一个也可以关闭两个。

(8)getsockname()和getpeername()

这两个函数与TCP连接建立过程中套接字地址结构的信息获取相关

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t *adrlen);
int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t *adrlen);
  • getsockname用于返回与某个套接字关联的本地协议地址
    • 没调用bind的TCP客户上,connect成功返回后,获取由内核赋予该连接的本地IP地址和端口号
    • 在以端口号0调用bind后,返回由内核赋予的本地端口号。
    • 获取某个套接字的地址簇
    • 绑定通配IP的服务器上,accept返回后,获取由内核赋予该连接的本地IP地址,此时sockfd不是监听套接字描述符
  • getpeername用于返回与某个套接字关联的外地协议地址
    • 当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能获取客户身份的唯一途径便是调用getpeername

(9)fork()

#include <unistd.h>
pid_t fork(void);
  • fork():调用进程(父进程)中返回一次,返回值是新建的子进程的进程ID号,在子进程又返回一次。返回值为0.返回值本身告知当前进程是父进程还是子进程。