参考:盛延敏:网络编程实战

TCP

TCP,又被叫做字节流套接字(Stream Socket),UDP 也有一个类似的叫法, 数据报套接字(Datagram Socket),一般分别以“SOCK_STREAM”与“SOCK_DGRAM”分别来表示 TCP 和 UDP 套接字。

Datagram Sockets 有时称为“无连接的 sockets”(connectionless sockets)。

Stream sockets 是可靠的、双向连接的通讯串流。比如以“1-2-3”的顺序将字节流输出到套接字上,它们在另一端一定会以“1-2-3”的顺序抵达,而且不会出错。这是由 TCP(Transmission Control Protocol)协议完成的,TCP 通过诸如连接管理,拥塞控制,数据流与窗口管理,超时和重传等一系列精巧而详细的设计,提供了高质量的端到端的通信方式。



详细的连接过程:

1)服务器端

① 创建套接字

创建可用的套接字

int socket(int domain, int type, int protocol)

入参解释:

domain:指 PF_INET、PF_INET6 以及 PF_LOCAL 等,表示什么样的套接字。

AF_LOCAL:表示的是本地地址,对应的是 Unix 套接字,这种情况一般用于本地 socket 通信,很多情况下也可以写成 AF_UNIX、AF_FILE;

AF_INET:因特网使用的 IPv4 地址;

AF_INET6:因特网使用的 IPv6 地址。这里的 AF_ 表示的含义是 Address Family,但是很多情况下,我们也会看到以 PF_ 表示的宏,比如 PF_INET、PF_INET6 等,实际上 PF_ 的意思是 Protocol Family,也就是协议族的意思。使用 AF_xxx 这样的值来初始化 socket 地址,用 PF_xxx 这样的值来初始化 socket。可在 <sys/socket.h> 头文件中可以清晰地看到,这两个值本身就是一一对应的。

/* 各种地址族的宏定义  */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6

type:

  • SOCK_STREAM: 表示的是字节流,对应 TCP;
  • SOCK_DGRAM: 表示的是数据报,对应 UDP;
  • SOCK_RAW: 表示的是原始套接字。

protocol:原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成。protocol 目前一般写成 0 即可。

② bind绑定套接字与套接字地址

调用 bind 函数的方式如下:

bind(int fd, sockaddr * addr, socklen_t len)

bind 函数后面的第二个参数是通用地址格式sockaddr * addr。传入的参数可能是 IPv4、IPv6 或者本地套接字格式。

bind 函数会根据 len 字段判断传入的参数 addr 该怎么解析,len 字段表示的就是传入的地址长度,它是一个可变值。

如何设置统配地址?

对于 IPv4 的地址来说,使用 INADDR_ANY 来完成通配地址的设置;

对于 IPv6 的地址来说,使用 IN6ADDR_ANY 来完成通配地址的设置。

struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4通配地址 */

端口设置呢?

如果把端口设置成 0,就相当于把端口的选择权交给操作系统内核来处理,操作系统内核会根据一定的算法选择一个空闲的端口,完成套接字的绑定。这在服务器端不常使用。

③ listen监听

初始化创建的套接字,可以认为是一个"主动"套接字,其目的是之后主动发起请求(通过调用 connect 函数)。通过 listen 函数,可以将原来的"主动"套接字转换为"被动"套接字,告诉操作系统内核:“我这个套接字是用来等待用户请求的。”当然,操作系统内核会为此做好接收用户请求的一切准备,比如完成连接队列。

listen函数原型:

int listen (int socketfd, int backlog)

参数详解:

第一个参数 socketdf 为套接字描述符,第二个参数 backlog,在 Linux 中表示已完成 (ESTABLISHED) 且未 accept 的队列大小,这个参数的大小决定了可以接收的并发数目。理论上,这个参数越大,并发数量也会越大。

④ accept响应

ccept 这个函数看成是操作系统内核和应用程序之间的桥梁。它的原型是:

int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)

函数的第一个参数 listensockfd 是套接字,可以叫它为 listen 套接字,因为这就是前面通过 bind,listen 一系列操作而得到的套接字。

函数的返回值有两个部分,第一个部分 cliadd 是通过指针方式获取的客户端的地址,addrlen 告诉我们地址的大小,这可以理解成当我们拿起电话机时,看到了来电显示,知道了对方的号码;另一个部分是函数的返回值,这个返回值是一个全新的描述字,代表了与客户端的连接。

PS:一定要注意有两个套接字描述字,第一个是监听套接字描述字 listensockfd,它是作为输入参数存在的;第二个是返回的已连接套接字描述字

accept之后,便建立了连接,然后便可收、发数据。

2)客户端发起连接请求的过程

第一步还是和服务端一样,要建立一个套接字,方法和前面是一样的。

不一样的是客户端需要调用 connect 向服务端发起请求。

① connect连接

函数原型:

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)

函数的第一个参数 sockfd 是连接套接字,通过前面讲述的 socket 函数创建。第二个、第三个参数 servaddr 和 addrlen 分别代表指向套接字地址结构的指针和该结构的大小。套接字地址结构必须含有服务器的 IP 地址和端口号。

如果是 TCP 套接字,那么调用 connect 函数将激发 TCP 的三次握手过程,而且仅在连接建立成功或出错时才返回。其中出错返回可能有以下几种情况:

  • 三次握手无法建立,客户端发出的 SYN 包没有任何响应,于是返回 TIMEOUT 错误。这种情况比较常见的原因是对应的服务端 IP 写错。
  • 客户端收到了 RST(复位)回答,这时候客户端会立即返回 CONNECTION REFUSED 错误。这种情况比较常见于客户端发送连接请求时的请求端口写错,因为 RST 是 TCP 在发生错误时发送的一种 TCP 分节。产生 RST 的三个条件是:目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器(如前所述);TCP 想取消一个已有连接;TCP 接收到一个根本不存在的连接上的分节。
  • 客户发出的 SYN 包在网络上引起了"destination unreachable",即目的不可达的错误。这种情况比较常见的原因是客户端和服务器端路由不通。

connect 之后便可进行读、写操作。

TCP 三次握手



目前,使用的网络编程模型都是阻塞式的。所谓阻塞式,就是调用发起后不会直接返回,由操作系统内核处理之后才会返回。 相对的,还有一种叫做非阻塞式的。

三次握手解读

  • 宏观过程:

    服务器端通过 socket,bind 和 listen 完成了被动套接字的准备工作,被动的意思就是等着别人来连接,然后调用 accept,就会阻塞在这里,等待客户端的连接来临;客户端通过调用 socket 和 connect 函数之后,也会阻塞。接下来的事情是由操作系统内核完成的,更具体一点的说,是操作系统内核网络协议栈在工作。

  • 微观过程

    1、客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端进入 SYNC_SENT 状态;

    2、服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;

    3、客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;

    4、应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

读写数据

经过前面服务端socket/bind/listen/accept,客户端socket/connnect 等操作,客户端与服务端建立了连接,连接建立的根本目的是为了数据的收发。

发送数据

发送数据时常用的有三个函数,分别是 write、sendsendmsg

ssize_t write (int socketfd, const void *buffer, size_t size)
ssize_t send (int socketfd, const void *buffer, size_t size, int flags)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)

三个函数使用场景略有不同:

  • 第一个函数是常见的文件写函数,如果把 socketfd 换成文件描述符,就是普通的文件写入。
  • 如果想指定选项,发送带外数据,就需要使用第二个带 flag 的函数。所谓带外数据,是一种基于 TCP 协议的紧急数据,用于客户端 - 服务器在特定场景下的紧急处理。
  • 如果想指定多重缓冲区传输数据,就需要使用第三个函数,以结构体 msghdr 的方式发送数据。
发送缓冲区

当 TCP 三次握手成功,TCP 连接成功建立后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区。

发送缓冲区的大小可以通过套接字选项来改变,当我们的应用程序调用 write 函数时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。

大部分 UNIX 系统的做法是一直等到可以把应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回。

可理解为:

当 TCP 连接建立之后,系统内核就开始运作起来。可以把发送缓冲区想象成一条包裹流水线,有个聪明且忙碌的工人不断地从流水线上取出包裹(数据),这个工人会按照 TCP/IP 的语义,将取出的包裹(数据)封装成 TCP 的 MSS 包,以及 IP 的 MTU 包,最后走数据链路层将数据发送出去。这样我们的发送缓冲区就又空了一部分,于是又可以继续从应用程序搬一部分数据到发送缓冲区里,这样一直进行下去,到某一个时刻,应用程序的数据可以完全放置到发送缓冲区里。在这个时候,write 阻塞调用返回。注意返回的时刻,应用程序数据并没有全部被发送出去,发送缓冲区里还有部分数据,这部分数据会在稍后由操作系统内核通过网络发送出去。

读取数据

read 函数的原型如下:

ssize_t read (int socketfd, void *buffer, size_t size)

read 函数要求操作系统内核从套接字描述字 socketfd读取最多多少个字节(size),并将结果存储到 buffer 中。

返回值为实际读取的字节数目,也有一些特殊情况,如果返回值为 0,表示 EOF(end-of-file),这在网络中表示对端发送了 FIN 包,要处理断连的情况;如果返回值为 -1,表示出错。如果是非阻塞I/O情况略有不同。

TCP socket编程(缓冲区实验)

服务端:tcp_server.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <string.h> size_t readn(int fd, void *buffer, size_t size) {
char *buffer_pointer = (char *)buffer;
int length = size; while (length > 0) {
int result = read(fd, buffer_pointer, length); if (result < 0) {
if (errno == EINTR)
continue; /* 考虑非阻塞的情况,这里需要再次调用read */
else
return (-1);
} else if (result == 0)
break; /* EOF(End of File)表示套接字关闭 */ length -= result;
buffer_pointer += result;
}
return (size - length); /* 返回的是实际读取的字节数*/
} void read_data(int sockfd)
{
ssize_t n;
char buf[1024]; int time = 0;
for(;;)
{
fprintf(stdout,"block in read\n"); if((readn(sockfd,buf,1024))==0)
return ; time++;
fprintf(stdout,"1K read for %d\n",time);
usleep(1000);
}
} int main(int argc,char *argv[])
{
int listenfd,connfd;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(12345);
//bind到本地地址,端口号12345
bind(listenfd, (struct sockaddr *)& servaddr, sizeof(servaddr)); //监听端口,backlog为1024
listen(listenfd,1024); //循环处理用户请求
for(;;)
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen);
read_data(connfd);//读取数据
close(connfd);//关闭连接套接字,主要不是监听套接字
} }

客户端:tcpclient.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <string.h> #define MESSAGE_SIZE 102400 void send_data(int sockfd)
{
char *query;
query = (char *)malloc(MESSAGE_SIZE+1);
for(int i = 0; i < MESSAGE_SIZE; i++)
{
query[i] = 'a';
}
query[MESSAGE_SIZE] = "\0"; const char *cp;
cp = query;
size_t remaining = strlen(query);
while(remaining)
{
int n_writen = send(sockfd, cp, remaining, 0);
fprintf(stdout, "send into buffer %ld \n",n_writen);
if(n_writen <= 0)
{
printf("send faild...\n");
return ;
}
remaining -= n_writen;
cp += n_writen;
}
return ;
} int main(int argc,char *argv[])
{
int sockfd;
struct sockaddr_in servaddr; if(argc != 2)
{
printf("argc:%d\n",argc);
printf("usage:tcpclient <IPaddress> \n");
return -1;
} sockfd = socket(AF_INET, SOCK_STREAM, 0);
if( sockfd < 0 )
{
perror("socket");
exit(EXIT_FAILURE);
} bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(12345);
inet_pton(AF_INET,argv[1], &servaddr.sin_addr);
int connect_rt = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if(connect_rt < 0)
{
printf("connect failed \n");
return -1;
}
send_data(sockfd);
exit(0);
}

效果:

程序运行起来后,服务端不断地在打印出读取字节流的过程:

而,客户端直到最后才打印下面一句话,说明在此之前send函数一直都是阻塞的,也就是说阻塞式套接字最终发送的实际写入字节数和请求字节数是相等的。

强调:

发送成功仅仅表示的是数据被拷贝到了发送缓冲区中,并不意味着连接对端已经收到所有的数据。至于什么时候发送到对端的接收缓冲区,或者更进一步说,什么时候被对方应用程序缓冲所接收,对我们而言完全都是透明的。

附:

为什么需要htons(), ntohl(), ntohs(),htons() 函数

网络编程:TCP 网络编程的更多相关文章

  1. C++网络套接字编程TCP和UDP实例

    原文地址:C++网络套接字编程TCP和UDP实例作者:xiaojiangjiang 1.       创建一个简单的SOCKET编程流程如下 面向有连接的套接字编程 服务器: 1)  创建套接字(so ...

  2. Linux下TCP网络编程与基于Windows下C#socket编程间通信

    一.linux下TCP网络编程基础,需要了解相关函数 Socket():用于套接字初始化. Bind():将 socket 与本机上的一个端口绑定,就可以在该端口监听服务请求. Listen():使s ...

  3. GO语言练习:网络编程 TCP 示例

    1.代码 2.编译及运行 1.网络编程 TCP 示例 simplehttp.go 代码 package main import ( "net" "os" &qu ...

  4. JAVA TCP网络编程学习笔记

    一.JAVA网络编程概述 网络应用程序,就是在已实现网络互联的不同计算机上运行的应用程序,这些程序之间可以相互交换数据.JAVA是优秀的网络编程语言,Java网络编程的类库位于java.net包中.J ...

  5. 网络编程TCP协议-聊天室

    网络编程TCP协议-聊天室(客户端与服务端的交互); <span style="font-size:18px;">1.客户端发数据到服务端.</span> ...

  6. C#网络程序设计(3)网络传输编程之TCP编程

        网络传输编程指基于各种网络协议进行编程,包括TCP编程,UDP编程,P2P编程.本节介绍TCP编程.     (1)TCP简介: TCP是TCP/IP体系中最重要的传输层协议,它提供全双工和可 ...

  7. 简述TCP网络编程本质

    基于事件的非阻塞网络编程是编写高性能并发网络服务程序的主流模式,头一次使用这种模式编程需要转换思维模式 .把原来的"主动调用recv()来接收数据,主动调用accept()来接受连接,主动调 ...

  8. tcp 网络编程

    网络编程同时也是进程间的一种通信:服务器进程和应用进程间的通信. OSI:开放式系统互联 OSI 7层模型:                                               ...

  9. UNIX网络编程——解决TCP网络传输“粘包”问题

    当前在网络传输应用中,广泛采用的是TCP/IP通信协议及其标准的socket应用开发编程接口(API).TCP/IP传输层有两个并列的协议:TCP和UDP.其中TCP(transport contro ...

  10. 【Linux网络编程】TCP网络编程中connect()、listen()和accept()三者之间的关系

    [Linux网络编程]TCP网络编程中connect().listen()和accept()三者之间的关系 基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下: conn ...

随机推荐

  1. 若依-Vue 单体版本 更换mybatisPlus

    1.单体模块在pom.xml ; 多模块版本在ruoyi-common\pom.xml.模块添加整合依赖 <!-- mybatis-plus 增强CRUD --> <dependen ...

  2. Scala查看源码

    package com.wyh.day01 /** * 1.代码格式化的快捷键 ctrl+alt+L\ * 2.scala查看源代码的快捷键 ctrl+b */ object ScalaLookSou ...

  3. Python基础笔记-while、字符串格式化、运算符、基础概念与数据类型

    前言 !!!注意:本系列所写的文章全部是学习笔记,来自于观看视频的笔记记录,防止丢失.观看的视频笔记来自于:哔哩哔哩武沛齐老师的视频:2022 Python的web开发(完整版) 入门全套教程,零基础 ...

  4. MySQL 8.0 语法记录

    SQL又杂又烦,记不住,网上搜到的语句还未必正确.这里做一个Record 基本操作 数据库操作 数据表操作 create index [索引名] on [表名]([列名]); /* 以选定列为索引信息 ...

  5. Java的数据类型详解

    java的为强类型语言,所以要求变量的使用要严格符合规定,所有的变量都必须先定义后在使用: 什么是变量? 变量顾名思义,就是可变的量:是程序中最基本的存储单元,其要素要包括:变量名.变量类型和作用域: ...

  6. c++中的类成员函数指针

    c++中的类成员函数指针 文章目录 c++中的类成员函数指针 发生的事情 正常的函数指针定义 定义类的成员函数指针 std::function 发生的事情 最近,想用一个QMap来创建字符串和一个函数 ...

  7. 【Java】各种代码块的执行顺序

    静态代码块:用staitc声明,jvm加载类时执行,仅执行一次 构造代码块:类中直接用{}定义,每一次创建对象时执行. 执行顺序优先级:静态块,main(),构造块,构造方法. 构造函数 public ...

  8. Code First 初始化数据时发生异常

    问题重现 用Entity Framework的Code First默认生成的数据库文件被我直接删除了, 然后不管怎么重新编译等等, 运行后总是会报错如下: 解决方案同下 Cannot attach t ...

  9. ASP.NET 日志路径

    默认路径 protected void Button_StreamWrite_Click(object sender, EventArgs e) {     StreamWriter sw = new ...

  10. 创建windows脚本bat/cmd或jar为windows服务完整教程

    ​ 一.将windows bat/cmd脚本创建为windows服务 1.下载winsw工具 https://gitee.com/colinisg/winsw/releases/download/v2 ...