背景

上一讲我们介绍了 基于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;
}

Linux 系统编程 学习:008-基于socket的网络编程3:基于 TCP 的通信的更多相关文章

  1. python核心编程学习(第三版)之网络编程

    套接字 套接字是计算机网络数据结构.在任何类型的通信开始之前,网络应用程序必须创建套接字. 有两种类型的套接字,基于文件和面向网络的. unix套接字是第一个家族,AF_UNIX代表地址家族,缩写AF ...

  2. Linux 系统编程 学习:06-基于socket的网络编程1:有关概念

    Linux 系统编程 学习:006-基于socket的网络编程1:有关概念 背景 上一讲 进程间通信:System V IPC(2)中,我们介绍了System IPC中关于信号量的概念,以及如何使用. ...

  3. Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信

    Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信 背景 上一讲我们介绍了网络编程的一些概念.socket的网络编程的有关概念 这一讲我们来看UDP 通信. 知识 U ...

  4. 一点点linux系统的学习心得

    我相信你正在阅读本文的时候,可能是因为你渴望学习Linux技术.我想分享一下过去两年中我自己的一些学习经历,希望你能更顺利地成为Linuxer. 两年前在Linux系统的运行和维护方面找到了一份工作( ...

  5. 安装虚拟机和Linux系统的学习

    安装虚拟机和Linux系统的学习(随笔3) 1.安装虚拟机 首先我按着老师给的链接上的步骤一步一步安装VirtualBox,进行得十分顺利. 接着则是在虚拟机上安装Ubuntu. 然而安装完成以后按要 ...

  6. Java网络编程和NIO详解9:基于NIO的网络编程框架Netty

    Java网络编程和NIO详解9:基于NIO的网络编程框架Netty 转自https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introd ...

  7. javaSE学习笔记(16)---网络编程

    javaSE学习笔记(16)---网络编程 基本概念 如今,计算机已经成为人们学习.工作.生活必不可少的工具.我们利用计算机可以和亲朋好友网上聊天,也可以玩网游.发邮件等等,这些功能实现都离不开计算机 ...

  8. Python学习day34-面向对象和网络编程总结

    figure:last-child { margin-bottom: 0.5rem; } #write ol, #write ul { position: relative; } img { max- ...

  9. centos Linux系统日常管理2 tcpdump,tshark,selinux,strings命令, iptables ,crontab,TCP,UDP,ICMP,FTP网络知识 第十五节课

    centos  Linux系统日常管理2  tcpdump,tshark,selinux,strings命令, iptables ,crontab,TCP,UDP,ICMP,FTP网络知识 第十五节课 ...

  10. 浅谈JAVA中如何利用socket进行网络编程(二)

    转自:http://developer.51cto.com/art/201106/268386.htm Socket是网络上运行的两个程序间双向通讯的一端,它既可以接受请求,也可以发送请求,利用它可以 ...

随机推荐

  1. 腾讯一面!说说ArrayList的遍历foreach与iterator时remove的区别,我一脸懵逼

    本文基于JDK-8u261源码分析 1 简介 ​ ArrayList作为最基础的集合类,其底层是使用一个动态数组来实现的,这里"动态"的意思是可以动态扩容(虽然ArrayList可 ...

  2. Redis小记(三)

    1.复制 通过slaveof命令或设置slaveof选项,实现一个服务器去复制另一个服务器,被复制的是主服务器,执行复制的是从服务器,复制过程中主从双方数据库保持数据一致 2.8版本以前,可分为初次复 ...

  3. PHP 7:真实世界的应用开发(中文翻译)

    前言 PHP 7:真实世界的应用开发(中文翻译) 作者:Doug Bierer, Altaf Hussain, Branko Ajzele 原书名称:<PHP 7: Real World App ...

  4. 第二次UML作业

    博客班级 https://edu.cnblogs.com/campus/fzzcxy/2018SE1/ 作业要求 https://edu.cnblogs.com/campus/fzzcxy/2018S ...

  5. 001 01 Android 零基础入门 01 Java基础语法 01 Java初识 01 导学

    001 01 Android 零基础入门 01 Java基础语法 01 Java初识 01 导学 welcome to Java World 欢迎来到Java世界 一起领略Java编程世界的奥秘与奥妙 ...

  6. C++ | 继承(基类,父类,超类),(派生类,子类)

    转载:https://blog.csdn.net/Sherlock_Homles/article/details/82927515 文章参考:https://blog.csdn.net/war1111 ...

  7. Hadoop理论基础

    Hadoop是 Apache 旗下的一个用 java 语言实现开源软件框架,是一个开发和运行处理大规模数据的软件平台.允许使用简单的编程模型在大量计算机集群上对大型数据集进行分布式处理.   特性:扩 ...

  8. 翻了翻element-ui源码,发现一个很实用的指令clickoutside

    前言 指令(directive)在 vue 开发中是一项很实用的功能,指令可以绑定到某一元素或组件,使功能的颗粒度更精细.今天在翻 element-ui 的源码时,发现一个还挺实用的工具指令,跟大伙分 ...

  9. Windows 系统蓝屏错误小全

    0 0x00000000 作业完成. 1 0x00000001 不正确的函数. 2 0x00000002 系统找不到指定的档案. 3 0x00000003 系统找不到指定的路径. 4 0x000000 ...

  10. 原生tab选项卡

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...