【计算网络学习笔记】TCP套接字介绍和使用

🔥个人主页 :大白的编程日记
🔥专栏:计算机网络学习笔记

文章目录
- 【计算网络学习笔记】TCP套接字介绍和使用
-
- 前言
- 1.TCP网络程序
-
- [1.1 TCP socket API 详解](#1.1 TCP socket API 详解)
- [1.2 补充命令](#1.2 补充命令)
- [1.3 nocopy.hpp](#1.3 nocopy.hpp)
- [1.4 TcpServer.hpp](#1.4 TcpServer.hpp)
- [1.5 TcpServer.cc](#1.5 TcpServer.cc)
- 后言
前言
哈喽,各位小伙伴大家好!上期我们讲了UDP 今天我们讲的是TCP套接字介绍和使用。话不多说,我们进入正题!向大厂冲锋!
1.TCP网络程序
和刚才 UDP 类似. 实现一个简单的英译汉的功能
1.1 TCP socket API 详解
下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中。

- socket():

cpp
NAME socket - create an endpoint for communication
SYNOPSIS #include <sys/types.h> /\* See NOTES \*/ #include <sys/shipment.h> int socket(int domain, int type, int protocol);
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;- 应用程序可以像读写文件一样用
read/write在网络上收发数据;
如果socket()调用出错则返回-1; - 对于 IPv4, family 参数指定为
AF_INET; - 对于 TCP 协议, type 参数指定为
SOCK_STREAM, 表示面向流的传输协议 protocol参数的介绍从略, 指定为 0 即可。- bind():

cpp
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
服务器程序所监听的网络地址和端口号通常是固定不变的, 客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号;
-
bind()成功返回0,失败返回-1。 -
bind()的作用是将参数sockaddr和myaddr绑定在一起, 使sockaddr这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;前面讲过,
struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度;
我们的程序中对myaddr参数是这样初始化的:
cpp
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htons(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
- 将整个结构体清零;
- 设置地址类型为
AF_INET; - 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址, 因为服务器可能有多个网卡, 每个网卡也可能绑定多个IP地址, 这样设置可以在所有的IP地址上监听, 直到与某个客户端建立了连接时才确定下来到底用哪个IP地址;
- 端口号为
SERV_PORT,我们定义为9999;
- listen():

cpp
NAME listen - listen for connections on a socket
SYNOPSIS #include <sys/types.h> /\* See NOTES \*/ #include <sys/socket.h> int listen(int sockfd, int backlog);
listen()声明sockfd处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大 (一般是 5), 具体细节同学们课后深入研究;listen()成功返回 0, 失败返回 -1;- accept():

cpp
NAME accept - accept a connection on a socket
SYNOPSIS include <sys/types.h> /See NOTES \*/ #include <sys/socket.h> int accept(int sockfd, struct sockaddr \*addr, socklen_t \*addrlen);
- 三次握手完成后,服务器调用
accept()接受连接;
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来; - addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给 addr 参数传 NULL, 表示不关心客户端的地址;
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
1.2 补充命令

我们的服务器程序结构是这样的:
cpp
while (1) {
cliaddr_len = sizeof(connfd);
connfd = accept(listenfd, (struct sockaddr *)&ciaddr, &ciaddr_len);
n = read(connfd, buf, MAXLINE);
...
close(connfd);
}
理解 accept 的返回值: 饭店拉客例子
- connect
c
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
- 客户端需要调用 connect() 连接服务器;
connect和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而 connect 的参数是对方的地址;connect()成功返回0,出错返回-1;

1.3 nocopy.hpp
cpp
#pragma once
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum ExitCode
{
OK = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR
};
class NoCopy
{
public:
NoCopy(){}
~NoCopy(){}
NoCopy(const NoCopy &) = delete;
const NoCopy &operator = (const NoCopy&) = delete;
};
#define CONV(addr) ((struct sockaddr*)&addr)
1.4 TcpServer.hpp
cpp
#include "Common.hpp"
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
const int defaultsockfd = -1;
const static int backlog = 6;
using task_t = function<void()>;
using fun_t = function<string(const string &, InetAddr &)>;
class TcpServer : public NoCopy
{
public:
TcpServer(const uint16_t &port, fun_t func)
: _port(port),
_listensockfd(defaultsockfd),
_func(func)
// _func(func)
{
}
void Init()
{
signal(SIGCHLD, SIG_IGN); // 忽略子进程结束的信号
// 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success:" << _listensockfd;
// 定义套接字转化类
InetAddr local("0", _port);
// 绑定套接字信息
int n = bind(_listensockfd, local.AddrPtr(), local.AddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success:" << _listensockfd;
// 设置监听模式接受连接
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success:" << _listensockfd;
}
class ThreadDate
{
public:
ThreadDate(int fd, InetAddr &ar, TcpServer *s)
: _sockfd(fd), _addr(ar), tsvr(s)
{
}
int _sockfd;
InetAddr _addr;
TcpServer *tsvr;
};
void Service(int sockfd, InetAddr &peer)
{
char buffer[1024];
while (1)
{
// read从套接字读取信息
char buffer[1024];
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
// 读取成功
if (n > 0)
{
buffer[n] = 0;
LOG(LogLevel::DEBUG) << peer.Message() << " say#" << buffer;
string echo_string = _func(buffer, peer); // 执行传入的回调模块
// string echo_string = "echo#";
// echo_string += buffer;
// 发送信息给客户端
write(sockfd, echo_string.c_str(), echo_string.size());
}
// 读取到末尾 客户端退出
else if (n == 0)
{
LOG(LogLevel::DEBUG) << peer.Message() << "退出了...";
close(sockfd);
break;
}
// 读取异常客户端异常
else
{
LOG(LogLevel::DEBUG) << peer.Message() << "异常...";
close(sockfd);
break;
}
}
}
// 设置为statice去掉this指针
static void *Route(void *args)
{
// 线程分离无需join阻塞等待
pthread_detach(pthread_self());
ThreadDate *td = static_cast<ThreadDate *>(args);
// 执行servse向用户发送消息
td->tsvr->Service(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
void Run()
{
_isrunning = 1;
while (_isrunning)
{
// sleep(1);
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 接受客户端发来的连接
int socket = accept(_listensockfd, CONV(peer), &len);
// 接收连接失败 继续接收
if (socket < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success peer addr:" << addr.Message();
// 多进程版本
pid_t id = fork();
if (id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
// 子进程
else if (id == 0)
{
// 子进程只需要执行server 不需要关心通信 关闭服务端描述符
close(_listensockfd);
// 再创建子进程 子进程直接退出
if(fork()>0)
{
exit(OK);
}
// 孙子进程发送消息给客户端
Service(socket, addr);
// 执行完系统自动回收孙子进程
exit(OK);
}
// 父进程
else
{
// 父进程只需要accepet接受 需要向客户端读写 关闭sockfd描述符
close(socket);
// 等待子进程 避免僵尸 但是此时会阻塞
pid_t rid = waitpid(id, nullptr, 0);
}
//多线程版
ThreadDate *td = new ThreadDate(socket, addr, this);
pthread_t tid;
// 创建线程执行发送任务
pthread_create(&tid, nullptr, Route, td);
// 线程池版
// 创建线程池传入执行任务
ThreadPool<task_t>::GetInstance()->Enqueue([this, socket, &addr]()
{
this->Service(socket, addr);
});
}
_isrunning = 0;
}
~TcpServer()
{
}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning = 0;
fun_t _func; // 设置回调处理
};
1.5 TcpServer.cc
cpp
#include"Common.hpp"
#include"TcpServer.hpp"
#include"Dict.hpp"
#include"Command.hpp"
using namespace Diction;
void Uage(char* argv[])
{
cout << "Usage:" << argv[0] << "ip port" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Uage(argv);
exit(USAGE_ERR);
}
uint16_t port=stoi(argv[1]);
Enable_Console_Log_Strategy();
//翻译模块
// Dict d;
// d.LodaDict();
// unique_ptr<TcpServer> tsvr=make_unique<TcpServer>(port,[&](const string& word,InetAddr& addr)
// {
// return d.Translate(word,addr);
// });
//命令行模块
Command cmd;
unique_ptr<TcpServer> tsvr=make_unique<TcpServer>(port,[&](const string& word,InetAddr& addr)
{
return cmd.Execute(word,addr);
});
tsvr->Init();
tsvr->Run();
return 0;
}

后言
这就是TCP套接字介绍和使用。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~

