背景

上一讲我们介绍了 基于UDP 的通信

这一讲我们来看 TCP 通信。

知识

TCP(Transmission Control Protoco 传输控制协议)。

TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式:

  • 基于流的方式;

  • 面向连接;

  • 可靠通信方式;

  • 在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销;

  • 通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。

为满足TCP协议的这些特点,TCP协议做了如下的规定:

  • 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
  • 到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认;
  • 超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片;
  • 滑动窗口:TCP连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出;
  • 失序处理:作为IP数据报来传输的TCP分片到达时可能会失序,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层;
  • 重复处理:作为IP数据报来传输的TCP分片会发生重复,TCP的接收端必须丢弃重复的数据;
  • 数据校验:TCP将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP将丢弃这个分片,并不确认收到此报文段导致对端超时并重发。
%% 时序图
sequenceDiagram
participant Server
participant Client

%% Note right of Client: Client主动连接->
Note right of Server: 双方都创建socket对象

Server ->> Server: socket
Client ->> Client: socket

Note left of Server: 服务器一般绑定端口号
Server ->> Server: bind

Note left of Server: 服务器监听是否有连接请求
Server ->> Server: listen

Note left of Client: 客户端请求链接
Client ->> Server: connect

Server ->> Server: accept

Note right of Server: 收发消息
Client -->> Server: send/recv
Server -->> Client: send/recv

Note right of Server: 关闭连接

Client --> Client: close
Server --> Server: close

有关函数介绍

根据流程图,我们知道,在UDP通信中,使用到了这些函数:socket()bind()sendto()recvfrom()

上面的函数我们在《基于UDP 的通信》 中已经讲过,这里不再重复了。

在TCP中,多了这几个函数:listen()connect()accept()

服务器调用listen 监听 客户端的 connectlisten成功时,服务器使用由accept获取到的新的套接字进行通信。

当客户端调用connect函数时,将引发三次握手过程:客户端首先发送SYN请求分组,此时服务端会将请求放入SYN队列,同时向客户端发送ACK确认报文,然后客户端向服务端再次发送ACK报文。服务端收到ACK确认报文后,将SYN里的连接请求移入ACCEPT队列。此时三次握手结束,即TCP连接成功建立。然后内核通知用户空间的阻塞的服务进程,服务进程调用accept仅仅是从ACCEPT队列里取出一个连接而已。也就是说客户端调用connect连接服务器,与服务器调用accept“接受”连接是两个独立的过程。

参考:《服务端不调用accept,客户端connect能否成功?》

listen

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

描述: 将尚未建立连接的socket转换为被动socket,并监听发给这个被动socket的connect请求。

参数解析:

sockfd:由socket函数成功返回的值

backlog :内核应该为相应套接口排队的最大连接个数(不是用来限制socket的最大连接数),一般为以下两个队列的大小之和,即未完成三次握手队列 + 已经完成三次握手队列。即:TCP模块允许的已完成三次握手过程(TCP模块完成)但还没来得及被应用程序accept的最大链接数。

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

1、未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接口处于SYN_RCVD状态。

2、已完成连接队列(completed connection queue),每个已完成TCP三次握手过程的客户对应其中一项。这些套接口处于ESTABLISHED状态。

当来自客户的SYN到达时,TCP在 未完成连接队列 中创建一个新项,然后响应以三次握手的第二个分节:服务器的SYN响应,其中稍带对客户SYN的ACK(即SYN+ACK)。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。

返回值: 成功返回0,失败返回-1,置errno:

  • EADDRINUSE:另一个套接字已在同一端口上侦听。
  • EADDRINUSE:(Internet域套接字)sockfd引用的套接字以前没有绑定到地址,在尝试将其绑定到临时端口时,确定临时端口范围中的所有端口号当前都在使用中。
  • EBADF:参数sockfd不是有效的描述符。
  • ENOTSOCK:文件描述符sockfd没有引用套接字。
  • EOPNOTSUPP:套接字的类型不支持listen()操作。

主动socket和被动socket

一般来说,使用socket函数创建的socket默认是主动socket,这意味着一个主动的socket可以调用connect跟一个被动socket建立一个连接,对主动socket来说,这叫主动打开。

被动socket是一个通过调用listen函数监听要发起连接的socket,当被动socket接受一个连接通常称为被动打开。

在大多数网络程序中,服务端会作为被动socket被动接受连接,而客户端会作为主动socket主动发起连接。

服务端通过socket函数创建的socket是主动socket,而listen函数就是把这个还未接受连接的主动socket转换为被动socket,因为服务端只需要被动接受客户端的连接请求。

Linux系统设置未连接队列最大数限制

linux系统tcp/ip协议栈有个选项可以设置未连接队列大小限制tcp_max_syn_backlog

可以通过命令:cat /proc/sys/net/ipv4/tcp_max_syn_backlog 查看

Linux 系统中提供somaxconn这个参数,它定义了系统中每一个端口最大的监听队列的长度,这是个全局的参数,默认值为128

可以通过命令: cat /proc/sys/net/core/somaxconn 查看

connect

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

描述: 连接一个被动socket

参数解析:

sockfd:主动socket

addr:目的地址

addrlen:地址属性的长度(addr的大小)

返回值:成功返回0,失败返回-1,置errno:

  • EAFNOSUPPORT:传递的地址在其sau family字段中没有正确的地址系列。
  • EAGAIN :路由缓存中的条目不足。
  • EALREADY:套接字未阻塞,上一次连接尝试尚未完成。

    EBADF:文件描述符不是描述符表中的有效索引。
  • ECONNREFUSED:没有人监听远程地址。
  • EFAULT :套接字结构地址在用户的地址空间之外。
  • EINPROGRESS:套接字未阻塞,无法立即完成连接。可以通过选择要写入的套接字来选择(2)或轮询(2)以完成。
  • EINTR :系统调用被捕获的信号中断。
  • EISCONN:套接字已连接。
  • ENETUNREACH:无法访问网络。
  • ENOTSOCK:sockfd不是套接字。
  • EPROTOTYPE:套接字类型不支持请求的通信协议。例如,在尝试将UNIX域数据报套接字连接到流套接字时,可能会发生此错误。
  • ETIMEDOUT:尝试连接时超时。服务器可能太忙,无法接受新连接。请注意,对于IP套接字,当服务器上启用Syncookie时,超时可能非常长。

accept

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); #define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h> int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);

描述: 从内核的ACCEPT队列中取出对应被动socket的连接,关于连接的有关属性填入addr中。

参数解析:

sockfd:对应的被动socket

addr:保存连接方的addr属性的容器

len:addr属性的长度

返回值: 成功返回可用于连接的新socket,失败返回-1,置errno:

此外,可能会返回新套接字的网络错误以及为协议定义的网络错误。各种Linux内核可以返回其他错误,例如ENOSR、ESOCKTNOSUPPORT、EPROTONOSUPPORT、ETIMEDOUT。在跟踪期间可以看到值ERESTARTSYS。

  • EMFILE :已达到打开的文件描述符数的每个进程限制

  • ENFILE :已达到系统范围内打开文件总数的限制

  • ENOBUFS, ENOMEM:没有足够的可用内存。这通常意味着内存分配受到套接字缓冲区限制,而不是系统内存的限制

  • ENOTSOCK sockfd不是套接字

  • EOPNOTSUPP 引用的套接字不是SOCK_STREAM类型

  • EPROTO :协议错误

  • EPERM (Linux) :防火墙规则禁止连接

例程

我们简单地进行一次TCP对答通信的实现

server.c

/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: server.c
# Created : Sat 21 Mar 2020 04:43:39 PM CST
*/ #include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> typedef struct _info {
char name[10];
char text[54];
}info; int main(int argc, char *argv[])
{
int my_socket;
unsigned int len;
int ret; // 创建套接字
my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
if(my_socket == -1) { perror("Socket"); }
printf("Creat a socket :[%d]\n", my_socket); // 用于接收消息
info buf ={0}; // 指定地址
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET; // 地址协议族
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //指定 IP地址
addr.sin_port = htons(12345); //指定端口号 // 服务器 绑定
bind(my_socket, (struct sockaddr *)&addr, sizeof(addr)); // my_socket 只用于监听
ret = listen(my_socket, 10);
if(-1 == ret) { perror("listen"); }
printf("Listening\n"); int new_socket;
struct sockaddr_in new = {0};
int new_addr_size;
// accept以后会返回一个新的套接字,用于与客户端通信
new_socket = accept(my_socket, (struct sockaddr*)&new, &new_addr_size);
printf("New socket is %d\n", new_socket);
perror("accept"); // 接收并打印消息
//recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
recv(new_socket, &buf, sizeof(buf), 0);
perror("recvfrom"); printf("%s: %s\n", buf.name, buf.text); // 回复消息
sprintf(buf.name, "Server");
sprintf(buf.text, "Had recvied your message");
//sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
send(new_socket, &buf, sizeof(buf), 0);
perror("sendto"); // 关闭连接
//shutdown(my_socket, SHUT_RDWR); perror("shutdown");
close(new_socket); perror("close");
return close(my_socket); perror("close");
printf("%d\n", errno);
return errno;
}

client.c

/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: client.c
# Created : Sat 21 Mar 2020 04:43:39 PM CST
*/ #include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> typedef struct _info {
char name[10];
char text[54];
}info; int main(int argc, char *argv[])
{
int my_socket;
unsigned int len;
int ret; // 创建套接字
my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
if(my_socket == -1) { perror("Socket"); }
printf("Creat a socket :[%d]\n", my_socket); // 用于接收消息
info buf ={0}; // 指定地址
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET; // 地址协议族
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //指定 IP地址
addr.sin_port = htons(12345); //指定端口号 // 用于连接服务器
connect(my_socket, (struct sockaddr *)(&addr), sizeof(struct sockaddr_in));
if(-1 == ret) { perror("connect"); }
printf("connected\n"); // 回复消息
sprintf(buf.name, "Client");
sprintf(buf.text, "Hello tcp text.");
//sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
send(my_socket, &buf, sizeof(buf), 0);
perror("sendto"); // 接收并打印消息
//recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
recv(my_socket, &buf, sizeof(buf), 0);
perror("recvfrom"); printf("%s: %s\n", buf.name, buf.text); // 关闭连接
//shutdown(my_socket, SHUT_RDWR); perror("shutdown"); return close(my_socket); perror("close");
printf("%d\n", errno);
return errno;
}

最新文章

  1. mac下安装及配置tomcat
  2. CentOS下配置java环境变量classpath
  3. bzoj3631: [JLOI2014]松鼠的新家(LCA+差分)
  4. LINQ系列:LINQ to SQL Exists/In/Any/All/Contains
  5. J2EE的13种核心技术
  6. Mysql有两种存储引擎:InnoDB与Myisam
  7. python escape sequences
  8. easyui源码翻译1.32--NumberBox(数值输入框)
  9. POJ 2777 Count Color(线段树+位运算)
  10. 多重bash登入的history写入问题
  11. 201521123039 《java程序设计》第八周学习总结
  12. FirstIDL
  13. springboot添加swagger2组件
  14. spark_to_kakfa
  15. CentOS下运行Java文件Error: Could not find or load main class
  16. 自动保存python一个项目的需求文件
  17. 使用C#的aforge类库识别验证码实例
  18. Windows下Redis缓存服务器的使用 .NET StackExchange.Redis Redis Desktop Manager 转发非原创
  19. App.js实现使用js开发app的应用,此文是中文文档
  20. Check类的validate方法解读

热门文章

  1. gateway(二、过滤器)
  2. Ribbon自定义负载均衡策略,在网关实现类似Ip_hash的负载均衡,ribbon给单个服务配置属性
  3. Mybatis 插件原理解析
  4. 给select赋值的一种方法
  5. DOS批处理中%cd%与%~dp0的区别详解
  6. C1853 编译器错误:fatal error C1853: &#39;pjtname.pch&#39; precompiled header file is from a previous
  7. selenium3+python3自动化环境搭建
  8. JavaScript返回当前的时分秒
  9. JAVA学习线路:day01面向对象(继承、抽象类)
  10. RHSA-2017:2473-重要: 内核 安全和BUG修复更新(需要重启、存在EXP、本地提权)