Linux 系统编程 学习:008-基于socket的网络编程3:基于 TCP 的通信
背景
上一讲我们介绍了 基于UDP 的通信
这一讲我们来看 TCP 通信。
知识
TCP(Transmission Control Protoco 传输控制协议)。
TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式:
-
基于流的方式;
-
面向连接;
-
可靠通信方式;
-
在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销;
-
通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。
为满足TCP协议的这些特点,TCP协议做了如下的规定:
- 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
- 到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认;
- 超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片;
- 滑动窗口:TCP连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出;
- 失序处理:作为IP数据报来传输的TCP分片到达时可能会失序,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层;
- 重复处理:作为IP数据报来传输的TCP分片会发生重复,TCP的接收端必须丢弃重复的数据;
- 数据校验:TCP将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP将丢弃这个分片,并不确认收到此报文段导致对端超时并重发。
有关函数介绍
根据流程图,我们知道,在UDP通信中,使用到了这些函数:socket()
、bind()
、sendto()
、recvfrom()
。
上面的函数我们在《基于UDP 的通信》 中已经讲过,这里不再重复了。
在TCP中,多了这几个函数:listen()
、connect()
、accept()
。
服务器调用listen
监听 客户端的 connect
;listen
成功时,服务器使用由accept
获取到的新的套接字进行通信。
当客户端调用connect函数时,将引发三次握手过程:客户端首先发送SYN请求分组,此时服务端会将请求放入SYN队列,同时向客户端发送ACK确认报文,然后客户端向服务端再次发送ACK报文。服务端收到ACK确认报文后,将SYN里的连接请求移入ACCEPT队列。此时三次握手结束,即TCP连接成功建立。然后内核通知用户空间的阻塞的服务进程,服务进程调用accept仅仅是从ACCEPT队列里取出一个连接而已。也就是说客户端调用connect连接服务器,与服务器调用accept“接受”连接是两个独立的过程。
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;
}
若在页首无特别声明,本篇文章由 Schips 经过整理后发布。
博客地址:https://www.cnblogs.com/schips/