目录
简介
此篇文章是我用来记录学习网络套接字的过程,代码是在linux操作系统的vim界面下基于C++编写的,如果在VS或者其他编译器出现提示不包含一些头文件,是正常的现象,因为其中的一些头文件是linux操作系统才有的(自己踩过的坑hhhh)。
TCP协议(准备知识 想看代码可直接跳过 重在记录)
TCP协议简介
TCP协议的英文全拼是Transmission Control Protocol,简称传输控制协议,与UDP相反,它是一种面向连接的、可靠的、基于字节流的传输层通信协议。(包含三次握手,四次挥手)
一般的通信步骤是:
1、创建连接:TCP不创建连接不传输数据,通信开始前一定要先建立好连接
2、传输数据
3、关闭连接
其余时间处于监听状态
TCP特性
通信双方必须先建立好连接才能进行数据传输,传输完数据的双方必须断开连接,不然会占用系统资源。主要的特性有
1、确定应答机制(ACK)
TCP协议规定只有ACK=1时,才能建立连接,也规定连接建立后所有发送报文的ACK必须是1;发送方发送一个ACK信号后,接收方可以反馈一个应答报文(ACK),表示自己已收到;
为什么要用这种应答方式呢?是因为在网络传输过程中,传输的消息有可能会出现消息本身数据错误或者顺序消息传输的顺序错误,导致客户端和服务端之间传输的数据可靠性得不到保证,出现“后发先至”或“先发后至”的情况。TCP协议的应答机制可以有效避免改情况的发现,针对发送的请求进行编号,应答的时候也针对编号进行应答,既能保证数据传输没有错误,也能保证传输顺序。
上述情况不是很严谨,因为真实的TCP不一样,TCP是面向字节流的,此处的编号并不是按照一两条来编的,而是按照字节来编号的,每个字节有一个编号。
确认应答是一种特殊的报文(ACK),所谓的应答报文,本质上就是 ACK 字段为1 的报文,此时报头中的"确认序号"字段才是生效的;
初始序号是随机的,为了防止网络攻击;如果发送多个数据,每个数据都会带着一个序号
接收方收到数据后,是知道数据所带着的序号的,根据序号给出确认序号(告诉发送方下次给我发的序号),发送给发送方,发送方就知道接收方收到了哪些数据
2、超时重传
但如果我给你发送了好久的消息,问你出不出来玩,你可能意念回复了,实际上一直没回,我感到很伤心,所以决定再发送一次问你,出不出来玩,并且铁了心就是要问到你回为止,所以我设置一个固定的时间,隔一段时间就给你发送一次消息,直到你回我 为止(生气)!这就是超时重传。
在实际数据传输中,会出现丢包的情况,比如
一、B不想回A的消息;
二、B没收到A的消息(请求消息丢失)
三、B回复了,但A没收到(应答ACK丢失)
为了应对二和三的丢包情况,TCP协议对这两种情况做了统一处理,在内部设置启动一个定时器,达到一定时间没有ACK反馈,定时器就会自动触发重传消息的动作--超时重传。
假设是第二种丢包情况,请求消息丢失,重传是没有问题的;但假如是第三种ACK丢失,重传会导致接收方收到了相同的数据,所以TCP会在内部进行数据去重(以序号为 key 进行去重)保证应用层读到的数据不是重复数据。
确认应答和超时重传是TCP可靠性中最核心的机制。
3、建立连接(三次握手)
TCP独特的建立连接方式,就是俗称的三次握手连接;
1、更好的保证可靠性: 建立连接的过程其实就是让通信双方验证各自的发送能力和接受能力是否正常
2、协商一些重要参数 (如: 序号的初始值)
举个例子:
第一次握手: 刚开始,A 不知道自己和 B 手机的听筒和话筒是否正常,所以 A说"喂,你能听到吗?"
第二次握手: B 听到后,说明 A 的话筒和 B 的听筒正常,但 B 还需进一步检查自己的话筒和 A 的听筒是否正常;同时 B 把 A 话筒正常和自己听筒正常的消息传递给 A;于是 B “我能听到,你呢?”
第三次握手: A 收到 B 的消息后,就证明了 A 听筒正常,B 话筒正常
以上三次握手就保证了 A、B 的听筒和话筒都正常,也就保证了通话的正常,这就类似于网络建立连接时的三次握手
ACK和SYN的简单说明:
1、ACK ,TCP协议规定只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1。
2、SYN,在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1,因此SYN置1就表示这是一个连接请求或连接接受报文。
3、FIN,用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
引用书本常用的一张图
建立连接的过程,相当于通信双方各自给对方发送 SYN,在各自给对方发送给 ACK,只不过中间的 ACK 和 SYN 合二为一了,于是最后就是"三次握手"
TCP建立连接过程中几个比较重要的状态:
LISTEN: 正在侦听来自远方的 TCP 端口的连接请求,服务端启动后处于 LISTEN 状态用于监听不同客户端的 TCP 请求并建立连接
SYN_SEND / SYN_RCVD: 建立连接的中间过程,若连接顺利的话(建立连接过程也可能丢包),这两个状态就一瞬消失
ESTABLISHEN: 连接建立完毕 (验证了通信双方的发送和接受能力都正常),可以进行数据传输
4、断开连接(四次挥手)
(1)当客户端没有东西要发送时就要释放连接的时候(注意这里首先提出中断连接端可以是客户端,也可以是服务端),客户端会发送一个FIN为1的没有数据的报文,进入FIN_WAIT状态,服务器收到后会给客户端一个确认,这时客户端那边不再发送数据信息(但仍可接收信息)。
(2)客户端收到服务器的确认后进入等待状态,等待服务器请求释放连接。 服务器数据发送完成后就向客户端请求连接释放(也是用FIN=1 表示,并且用ack = u+1(如图)), 客户端收到后回复一个确认信息,又要进入 TIME_WAIT 状态(等待2MSL 时间,最大报文生存时间)。服务器收到后关闭连接。
最后这里为什么还要等待呢?是防止最后一个ACK的丢失,服务器在超时后会重新发送FIN。如果客户端这时收到FIN就知道最后一个ACK丢失了,会重发。否则客户端等待一段时间后依然没有收到回复,则证明服务端端已正常关闭,那好,我客户端也可以关闭连接了。
四次挥手
值得注意的是,服务器存在一个保活状态,即如果客户端突然故障死机了,那B那边的连接资源什么时候能释放呢?就是保活时间到了后,B会发送探测信息,以决定是否释放连接。
关闭连接时,当Server端收到FIN报文时,很可能数据信息没有传完并不会立即关闭连接,所以只能先回复一个ACK报文(告诉客户端,"你发的FIN报文我收到了")。只有等到服务端端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步挥手。
TCP可以做什么
类似聊天信息传输,推送;单人语言,视频聊天;几乎UDP能做到的TCP都能做,但需要考虑复杂性,性能问题。
限制:无法进行广播,多播,搜索等(解决建议:考虑UDP协议)
简单TCP网络程序
服务端
服务端配置流程
创建服务端套接字函数
首先是创建socket套接字接口,实际上就是调用linux系统中封装好的函数
创建套接字:
int socket(int domain, int type, int protocol)
domain:地址域类型-AF_INET-表示IPV4版本通信;
type:套接字类型-SOCK_STREAM-表示流式套接字
protocol:协议类型-0默认在SOCK_STREAM下表示TCP协议,IPPROTO_TCP
返回值:成功返回一个非负整数,--操作句柄--套接字描述符;失败返回-1;
服务端绑定
绑定地址信息:
int bing(int sockfd, struct sockaddr *addr, socklen_t len);
sockfd:套接字描述符-创建套接字返回的操作句柄
addr:要绑定的地址信息,IPV4通信应该使用struct sockaddr_in结构
len:地址信息长度
返回值:成功返回0;失败返回-1;
服务端监听
设置套接字为监听状态的函数叫做listen,该函数的函数原型如下:
int listen(int sockfd, int backlog)
sockfd: 要使之开始监听的套接字描述符
backlog: 服务端在同一时间内能处理的最大客户端连接请求数量.一般设置5或者10就行
(SYN泛洪攻击: 恶意伪造ip地址, 向服务器发送大量连接请求, 这样服务器就会不断的创建大量的通信套接字, 不如不做最大连接请求数量, 有可能瞬间资源耗尽导致系统崩溃.)
返回值: 成功返回0, 失败返回-1.
服务端获取连接
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。
int accept(int sockfd, struct sockaddr* addr, socklen_t* len)
sockfd: 监听套接字描述符(监听套接字就是只用来处理连接请求复制出新套接字的套接字)
addr: 客户端的地址信息, 将该客户端的地址信息放入这个addr中.
len: 输入输出参数, 指定要获取的地址长度, 以及返回实际获取的信息长度
返回值: 成功返回新建连接的通信套接字描述符, 失败返回-1.
服务端处理请求
TCP服务器读取数据的函数叫做read,该函数的函数原型如下:
ssize_t read(int fd, void *buf, size_t count);
fd:特定的文件描述符,表示从该文件描述符中读取数据。
buf:数据的存储位置,表示将读取到的数据存储到该位置。
count:数据的个数,表示从该文件描述符中读取数据的字节数。
返回值说明:
如果返回值大于0,则表示本次实际读取到的字节个数。
如果返回值等于0,则表示对端已经把连接关闭了。
如果返回值小于0,则表示读取时遇到了错误。
TCP服务器写入数据的函数叫做write,该函数的函数原型如下:
ssize_t write(int fd, const void *buf, size_t count);
fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
buf:需要写入的数据。
count:需要写入数据的字节个数。
返回值:写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
客户端
客户端配置流程
创建客户端套接字函数
创建套接字:
int socket(int domain, int type, int protocol)
domain:地址域类型-AF_INET-表示IPV4版本通信;
type:套接字类型-SOCK_STREAM-表示流式套接字
protocol:协议类型-0默认在SOCK_STREAM下表示TCP协议,IPPROTO_TCP
返回值:成功返回一个非负整数,--操作句柄--套接字描述符;失败返回-1;
ps:与服务端创建方法一致.
客户端连接服务器
发起连接请求的函数叫做connect,该函数的函数原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:特定的套接字,表示通过该套接字发起连接请求。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入的addr结构体的长度。
返回值说明:连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。
客户端发起请求
由于实现的是一个简单的回声服务器,因此当客户端连接到服务端后,客户端就可以向服务端发送数据了,这里我们可以让客户端将用户输入的数据发送给服务端,发送时调用write函数向套接字当中写入数据即可。
当客户端将数据发送给服务端后,由于服务端读取到数据后还会进行回显,因此客户端在发送数据后还需要调用read函数读取服务端的响应数据,然后将该响应数据进行打印,以确定双方通信无误。
在运行客户端程序时我们就需要携带上服务端对应的IP地址和端口号,然后我们就可以通过服务端的IP地址和端口号构造出一个客户端对象,对客户端进行初始后启动客户端即可。
代码
服务端代码
#include<iostream>
#include<string>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<cstring>
#define BACKLOG 5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
class TcpServer
{
public:
TcpServer(int port)
: _sock(-1)
, _port(port)
{}
void InitServer()
{
_listen_sock = socket(AF_INET,SOCK_STREAM,0);
if (_listen_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
struct sockaddr_in local;
memset(&local,'\0',sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local))<0){
std::cerr << "bind error" << std::endl;
exit(3);
}
if(listen(_listen_sock,BACKLOG) < 0){
std::cerr << "listen error" << std::endl;
exit(4);
}
}
void Service(int sock, std::string client_ip, int client_port){
char buffer[1024];
while(true){
ssize_t size = read(sock,buffer, sizeof(buffer)-1);
if (size > 0){
buffer[size] = '\0';
std::cout << "get a new link-> " << sock << "[" << client_ip << "]" << client_port << std::endl;
write(sock, buffer, size);
}
else if(size == 0){
std::cout << client_ip << ":" << client_port << "close!" << std::endl;
break;
}
else{
std::cerr << sock << "read error!" << std::endl;
break;
}
}
close(sock);
std::cout << client_ip << ":" << client_port << "service done!" << std::endl;
}
void Start()
{
for(;;){
//
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
std::cerr << "accept error, continue next" << std::endl;
continue;
}
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
std::cout << "get a new link-> " << "[" << client_ip << "]" << client_port << std::endl;
Service(sock, client_ip, client_port);
}
}
private:
int _sock;
int _listen_sock;
int _port;
};
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 2){
Usage(argv[0]);
exit(1);
}
int port = atoi(argv[1]);
TcpServer* svr = new TcpServer(port);
svr->InitServer();
svr->Start();
return 0;
}
客户端代码
#include<iostream>
#include<string>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<cstring>
class TcpClient
{
public:
TcpClient(std::string server_ip, int server_port)
: _sock(-1)
, _server_ip(server_ip)
, _server_port(server_port)
{}
void InitClient()
{
//创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
}
void Request()
{
std::string msg;
char buffer[1024];
while (true){
std::cout << "Please Enter# ";
getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
ssize_t size = read(_sock, buffer, sizeof(buffer)-1);
if (size > 0){
buffer[size] = '\0';
std::cout << "server echo# " << buffer << std::endl;
}
else if (size == 0){
std::cout << "server close!" << std::endl;
break;
}
else{
std::cerr << "read error!" << std::endl;
break;
}
}
}
void Start()
{
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == 0){ //connect success
std::cout << "connect success..." << std::endl;
Request(); //发起请求
}
else{ //connect error
std::cerr << "connect failed..." << std::endl;
exit(3);
}
}
private:
int _sock; //套接字
std::string _server_ip; //服务端IP地址
int _server_port; //服务端端口号
};
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "server_ip server_port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3){
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
int server_port = atoi(argv[2]);
TcpClient* clt = new TcpClient(server_ip, server_port);
clt->InitClient();
clt->Start();
return 0;
}
运行结果
编译代码
运行结果
服务端
运行客户端代码
查看服务器
尝试连接该服务器发送消息,
客户端
tip:由于8888端口被其他人占用了,接下来端口用8889演示
创建个服务器
客户端连接该服务器并能简单收发
结束语
仅以此篇文章记录我之前有关socket接口的学习,如有错误请指正。不喜欢请轻点喷hhhh