系列文章目录
文章目录
- 系列文章目录
- 一、服务端实现
- 1.1 创建套接字socket
- 1.2 指定网络接口并bind
- 2.3 设置监听状态listen
- 2.4 获取新链接accept
- 2.5 接收数据并处理(服务)
- 2.6 整体代码
- 二、客户端实现
- 2.1 创建套接字socket
- 2.2 指定网络接口
- 2.3 发起链接connect
- 2.4 发送数据并接收
- 2.5 整体代码
- 2.6 绑定问题
- 三、问题与改进
- 3.1 问题描述
- 3.2 解决方法
- 3.2.1 问题版本
- 3.2.2 多进程版
- 3.4.3 进程池(暂未实现)
- 3.4.4 多线程版
- 3.4.5 线程池版
- 四、TCP与UDP的对比
一、服务端实现
1.1 创建套接字socket
和上篇文章UDP的使用一致,创建套接字
调用系统接口socket函数,帮助我们创建套接字,本质是把文件和网卡关联起来
参数介绍:
domain
:一个域,标识了这个套接字的通信类型(网络或者本地)
只用关注上面三个类,第一个与第二个
AF_UNIX/AF_LOCAL
表示本地通信,而AF_INET
表示网络通信
type
:套接字提供服务的类型
我们用UDP实现,所以使用SOCK_DGRAM
protocol
:想使用的协议,默认为0即可,因为前面的两个参数决定了,就已经决定了是TCP还是UDP协议了
返回值:
成功则返回打开的文件描述符(指向网卡文件,其实就是文件描述符),失败返回-1
创建套接字的本质其实就是创建了一个文件描述符,并返回该文件描述符的值
只是该文件描述符是用于对应服务的网路数据传输
// 1. 创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){lg.LogMessage(Fatal, "socket error, sockfd : %d\n", _listensock);exit(1);}lg.LogMessage(Info, "socket success, sockfd : %d\n", _listensock);
1.2 指定网络接口并bind
和上篇文章UDP的使用一致,服务端需要手动bind
参数介绍:
socket
:创建套接字的返回值
address
:通用结构体(上一章Linux网络——网络套接字有详细介绍)
address_len
:传入结构体的长度
我们要先定义一个sockaddr_in
结构体,将结构体内对应的字段填充好,再将结构体作为参数传递
struct sockaddr_in {short int sin_family; // 地址族,一般为AF_INET或PF_INETunsigned short int sin_port; // 端口号,网络字节序struct in_addr sin_addr; // IP地址unsigned char sin_zero[8]; // 用于填充,使sizeof(sockaddr_in)等于16
};
创建结构体后要先清空数据(初始化),我们可以用memset,也可以用系统接口:
#include <strings.h>void bzero(void *s, size_t n);
填充端口号的时候要注意端口号是两个字节的数据,涉及到大小端问题
在计算机中的普遍规定:在网络中传输的数据都是大端的
所以为了统一,无论我们机器是大端还是小端,在调用接口的时候,都将IP与端口号从主机序列转化为网路序列
端口号的接口
#include <arpa/inet.h>
// 主机序列转网络序列
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
// 网络序列转主机序列
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
IP的接口
对于IP,其实有两步:首先将字符串转换为整型,再解决大小端问题
系统给了直接能解决这两个问题的接口
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);in_addr_t inet_addr(const char *cp);// 点分十进制字符串in_addr_t inet_network(const char *cp);char *inet_ntoa(struct in_addr in);struct in_addr inet_makeaddr(int net, int host);in_addr_t inet_lnaof(struct in_addr in);in_addr_t inet_netof(struct in_addr in);
这里的inet_addr
就是把一个点分十进制的字符串转化成整数再进行大小端处理
代码:
// 2. 指定网络接口并bindstruct 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;int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local));if (n != 0){lg.LogMessage(Fatal, "bind error\n");exit(2);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);
2.3 设置监听状态listen
这里TCP跟UDP有所不同
要把socket套接字的状态设置为listen状态,只有这样才能一直获取新链接,接收新的链接请求
举个例子:
我们买东西如果出现了问题会去找客服,如果客服不在那么就回复不了,所以规定了客服在工作的时候必须要时刻接收回复消息,这个客服所处的状态就叫做监听状态
关于第二个参数backlog后边讲TCP协议的时候介绍,目前先直接用
const static int default_backlog = 1;// 3. 设置socket为监听状态int m = listen(_listensock, default_backlog);if (m != 0){lg.LogMessage(Fatal, "listen error\n");exit(3);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);
做完这些,初始化工作就完成了,总代码
void Init(){// 1. 创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){lg.LogMessage(Fatal, "socket error, sockfd : %d\n", _listensock);exit(1);}lg.LogMessage(Info, "socket success, sockfd : %d\n", _listensock);// 2. 指定网络接口并bindstruct 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;int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local));if (n != 0){lg.LogMessage(Fatal, "bind error\n");exit(2);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 3. 设置socket为监听状态int m = listen(_listensock, default_backlog);if (m != 0){lg.LogMessage(Fatal, "listen error\n");exit(3);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}
2.4 获取新链接accept
上面初始化完毕,现在开始就是要运行服务端,而TCP不能直接发数据,因为它是面向链接的,必须要先建立链接。
参数介绍:
sockfd
文件描述符,找到套接字
addr
输入输出型参数,是一个结构体,用来获取客户端的信息
addrlen
输入输出型参数,客户端传过来的结构体大小
返回值:
成功返回一个文件描述符
失败返回-1
而我们知道sockfd本来就是一个文件描述符,那么这个返回的文件描述符是什么呢?
举个例子:
我们去吃饭的时候会发现每个店铺门口都会有人来招揽顾客,这个人把我们领进去门店后,然后他就会继续站在门口继续招揽顾客,而我们会有里面的服务员来招待我们,给我们提供服务
这里的揽客的人就是_listensock,而服务员就是返回值的文件描述符
意思就是_listensock的作用就是把链接从底层获取上来,返回值的作用就是跟客户端通信
从这里就知道了成员变量中的_listensock`并不是通信用的套接字,而是专门用来获取链接的套接字
void Start(){_is_running = true;while (_is_running){// 4. 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);if (sockfd < 0){lg.LogMessage(Fatal, "socket accept error");continue;}lg.LogMessage(Debug, "accept socket success, get a new sockfd: %d\n", sockfd);Service(sockfd);close(sockfd);}}
2.5 接收数据并处理(服务)
当客户访问服务器的时候,必定是想要完成某件事,并且得到某件事完成的结果,我们称这个过程为服务
服务端收到客户端发来的信息或请求后,进行分析判断,完成客户端想要完成的任务,并返回给客户端
我们这里写一个简单的运用,此处的服务就是读取发来的信息并发回给客户端
void Service(int sockfd){char buffer[1024];while (true){int n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0) // read如果返回值是0,表示读到了文件结尾(对端关闭了连接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error");break;}}}
2.6 整体代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "Log.hpp"
#include "nocopy.hpp"static const int default_fd = -1;
const static int default_backlog = 1;class TcpServer : public nocopy
{
public:TcpServer(const uint16_t port): _port(port), _listensock(default_fd), _is_running(false){}void Init(){// 1. 创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){lg.LogMessage(Fatal, "socket error, sockfd : %d\n", _listensock);exit(1);}lg.LogMessage(Info, "socket success, sockfd : %d\n", _listensock);// 2. 指定网络接口并bindstruct 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;int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local));if (n != 0){lg.LogMessage(Fatal, "bind error\n");exit(2);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 3. 设置socket为监听状态int m = listen(_listensock, default_backlog);if (m != 0){lg.LogMessage(Fatal, "listen error\n");exit(3);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}void Service(int sockfd){char buffer[1024];while (true){int n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0) // read如果返回值是0,表示读到了文件结尾(对端关闭了连接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error");break;}}}void Start(){_is_running = true;while (_is_running){// 4. 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);if (sockfd < 0){lg.LogMessage(Fatal, "socket accept error");continue;}lg.LogMessage(Debug, "accept socket success, get a new sockfd: %d\n", sockfd);Service(sockfd);close(sockfd);}}~TcpServer(){}private:uint16_t _port;int _listensock;bool _is_running;
};#include "TcpServer.hpp"
#include <memory>void Usage(const std::string s)
{std::cout << "Usagr: " << s << " local_port" << std::endl;
}int main(int argc,char *argv[])
{if (argc != 2){Usage(argv[0]);return 1;}std::unique_ptr<TcpServer> tcpser = std::make_unique<TcpServer>(std::stoi(argv[1]));tcpser->Init();tcpser->Start();return 0;
}
二、客户端实现
2.1 创建套接字socket
// 1. 创建socketint _sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){lg.LogMessage(Fatal, "socket error, sockfd : %d\n", _sockfd);exit(1);}lg.LogMessage(Info, "socket success, sockfd : %d\n", _sockfd);
2.2 指定网络接口
// 2. 指定网络接口struct sockaddr_in send;memset(&send, 0, sizeof(send));send.sin_family = AF_INET;send.sin_port = htons(stoi(argv[2]));send.sin_addr.s_addr = inet_addr(argv[1]);
2.3 发起链接connect
参数说明:
这里的
addr
和addrlen
填入的是服务端信息
在UDP通信中,客户端在sendto的时候会自动绑定IP和port,TCP这里就是在connect的时候绑定
// 3. 进行连接int n = connect(_sockfd, (struct sockaddr *)&send, sizeof(send));
2.4 发送数据并接收
while (true){std::string inbuffer;std::cout << "Please Enter# ";getline(cin, inbuffer);int n = write(_sockfd, inbuffer.c_str(), inbuffer.size());if (n > 0){char buffer[1024];int m = read(_sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;cout << "get a echo messsge -> " << buffer << endl;}else if (m == 0 || m < 0){break;}}else{break;}}
2.5 整体代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "Log.hpp"void usage(std::string s)
{std::cout << "Usagr: " << s << " server_ip server_port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);return 0;}// 1. 创建socketint _sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){lg.LogMessage(Fatal, "socket error, sockfd : %d\n", _sockfd);exit(1);}lg.LogMessage(Info, "socket success, sockfd : %d\n", _sockfd);// 2. 指定网络接口struct sockaddr_in send;memset(&send, 0, sizeof(send));send.sin_family = AF_INET;send.sin_port = htons(stoi(argv[2]));send.sin_addr.s_addr = inet_addr(argv[1]);// 3. 进行连接int n = connect(_sockfd, (struct sockaddr *)&send, sizeof(send));while (true){std::string inbuffer;std::cout << "Please Enter# ";getline(cin, inbuffer);int n = write(_sockfd, inbuffer.c_str(), inbuffer.size());if (n > 0){char buffer[1024];int m = read(_sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;cout << "get a echo messsge -> " << buffer << endl;}else if (m == 0 || m < 0){break;}}else{break;}}return 0;
}
2.6 绑定问题
首先bind的作用是允许应用程序指定一个端口号用于监听传入的数据报或数据流
对于服务端:
需要绑定一个公开的端口号,允许大家访问,如果是随机的,其他人不知道,也就访问不了,所以服务端需要绑定
对于客户端:
客户端在给服务器发信息的同时,服务器也可能给客户端发信息,所以客户端为了监听服务器有没有给自己回信息也需要bind一个端口号用于监听,但客户端不需要显式的bind,因为客户端的端口号不会被所有人访问,别人不需要知道,所以OS自动帮我们bind一个随机的端口号,另外也是为了防止端口号重复,避免破坏唯一性
那么为什么前面服务端必须显示的绑定port呢?
因为服务器的端口号是众所周知的,不能改变,如果变了就找不到服务器了
而客户端只需要有就可以,只用标识唯一性即可
举个例子:
我们手机上有很多的app,而每个服务端是一家公司写的,但是客户端却是多个公司写的
如果我们绑定了特定的端口,万一两个公司都用了同一个端口号呢?这样就直接冲突了
OS会自动填充主机IP和随机生成端口号进行绑定(在发送数据的时候自动绑定)
所以创建客户端我们只用创建套接字即可
三、问题与改进
3.1 问题描述
上述图片是服务端的代码,由于整个服务端都是单进程的,所以这意味着在同一时间只能有一个客户端的链接能被accept,其他客户端的信息是接受不到的,因为一但某个客户端被成功accept了,那么单进程就会走到Service中,Service是一个死循环服务,所以只要客户端不退出,服务端是无法accept到其他链接的
3.2 解决方法
3.2.1 问题版本
3.2.2 多进程版
使用多进程,主进程进行accept,子进程进行Service服务
引入新问题:
主进程需要阻塞等待子进程退出,还是accept不了新连接
两个解决办法
- 用孙子进程执行服务
pid_t pid = fork();if (pid < 0){lg.LogMessage(Fatal, "fork error");close(sockfd);continue;}else if (pid == 0){close(_listensock);if (fork() == 0){Service(sockfd);close(sockfd);exit(0);}exit(0);}close(sockfd);waitpid(pid, nullptr, 0);
- 自定义信号,让父进程忽略子进程结束时发送给父进程的信号
signal(SIGCHLD,SIG_IGN);pid_t pid = fork();if (pid < 0){lg.LogMessage(Fatal, "fork error");close(sockfd);continue;}else if(pid == 0){close(_listensock);Service(sockfd);close(sockfd);exit(0);}close(sockfd);
3.4.3 进程池(暂未实现)
存在问题:
如果先创建子进程备用,子进程拿不到主进程accept的sockfd
3.4.4 多线程版
篇幅较长,Gitee连接:多线程版
3.4.5 线程池版
篇幅较长Gitee连接:线程池版
四、TCP与UDP的对比
对比UDP服务器,TCP服务器多了获取新链接和监听的操作
因为UDP是不可靠传输,而TCP是是可靠传输,所以TCP在传输数据之前会进行连接的建立
UDP和TCP的区别:
对于TCP协议有几个特点:
- 传输层协议
- 有连接(正式通信前要先建立连接)
- 可靠传输(在内部帮我们做可靠传输工作)
- 面向字节流
对于UDP协议有几个特点:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
注意:
这里的可靠与不可靠并不是贬义词,而是中性词,可靠或者不可靠形容的是特点,而不是优劣
并不是说可靠传输就好用,而是要区分应用场景和需求