赞
踩
目录
初始化服务端
和UDP创建套接字的方式差不多,只不过我们要实现的是TCP第二个参数选用:SOCK_STREAM
服务端关闭绑定失败的问题通常出现在服务端程序退出后,重新启动时,可能会因为之前的套接字仍然处于 TIME_WAIT 状态,导致无法绑定到相同的地址和端口。解决这个问题的方法通常是通过设置 SO_REUSEADDR
和 SO_REUSEPORT
选项。
- int opt = 1;
- setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
和UDP填充本地网络信息的方式一样,还是要注意以下几点:
- 端口号主机序列转网络序列
- IP地址点分十进制字符串转四字节网络序列
- 设置任意的IP地址
和UDP绑定一样,要注意结构体的强转
因为TCP要建立连接,一般是由客户端发起请求,服务端等待请求的到来。所以服务端要设置监听状态
int listen(int sockfd, int backlog);
返回值
listen()
函数的返回值是一个整数,表示执行结果的状态:
errno
来指示具体的错误类型。参数
sockfd
:是一个已经通过 socket()
函数创建的套接字描述符,即要进行监听的套接字。
backlog
:是一个整数,指定在内核中等待处理的连接队列的最大长度。这个参数的具体含义是系统内核在拒绝新连接之前允许处于未连接状态(SYN_RCVD
)的连接数量。
如果队列满了,新的连接会被拒绝,并且客户端可能会收到 ECONNREFUSED
错误。较大的 backlog
值可以容纳更多的等待连接的客户端,但同时也会增加系统资源的消耗。
如果 backlog
设置为 0,表示不接受连接队列,新连接会立即被拒绝。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
启动服务端
accept()
函数用于接受客户端的连接请求,并创建一个新的套接字来处理该连接。这个新的套接字是与客户端建立的连接的专用套接字,通过它可以进行数据的收发。
返回值
accept()
函数的返回值是一个整数,表示新创建的套接字的文件描述符:
errno
来指示具体的错误类型。函数调用成功后的返回值是作为我们接下来进行读取数据的文件描述符,旧的套接字依旧作为监听套接字。
参数
sockfd
:是一个已经通过 socket()
函数创建并调用 bind()
函数绑定了地址的监听套接字描述符,即待处理连接的监听套接字。
addr
:是一个指向 struct sockaddr
类型的指针,用于存储客户端的地址信息。这个参数是一个输出参数,accept()
函数会填写客户端的地址信息到这个结构体中。如果不需要知道客户端的地址,可以将这个参数设置为 NULL
。
addrlen
:是一个指向 socklen_t
类型的指针,用于指定 addr
结构体的长度。在调用 accept()
函数之前,需要将 addrlen
设置为 addr
结构体的长度。这个参数也是一个输入输出参数,accept()
函数会将实际的客户端地址长度写入到这个变量中。
因为建立连接后得到一个文件描述符,因此我们可以用使用文件的读写方法来进行我们的收取和反馈消息的操作。
TCPserver.hpp
- #pragma once
- #include <iostream>
- #include <string>
- #include <cstring>
- #include <strings.h>
- #include <cerrno>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- using namespace std;
- const static int default_backlog = 5;
- class TcpServer
- {
- public:
- TcpServer(uint16_t port)
- : _port(port), _isrunning(false)
- {
- }
- // 初始化服务器
- void Init()
- {
- // 第一步:套接字创建
- // 得到文件描述符
- // 本质是文件
- // 第一个参数表示域
- // 第二个参数表示套接字类型
- // 第三个参数为协议 默认缺省
- _listensock = socket(AF_INET, SOCK_STREAM, 0);
- if (_listensock < 0)
- {
- cout << "Fatal Error" << endl
- << "create socket error, errno code %d ,eror string %d",
- errno, strerror(errno);
- cout << endl;
- exit(2);
- }
- cout << "create socket success, sockfd : " << _listensock << endl;
- int opt=1;
- setsockopt(_listensock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
- //解决服务端关闭绑定失败问题
- // 第二步:填充本地网络信息并且绑定和监听
- 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(_listensock, (struct sockaddr *)&local, sizeof(local)) != 0)
- {
- cout << "bind Error" << "errno code : " << errno << "error string : " << strerror(errno) << endl;
- exit(3);
- }
- cout << "bind success" << endl;
- // Tcp需要建立连接,一般是由客户端发起请求 服务端一直等待连接的到来
- // Tcp需要监听
- // 设置套接字为监听状态
- if (listen(_listensock, default_backlog) != 0)
- {
- cout << "listen Error" << "errno code: " << errno << "error string : " << strerror(errno) << endl;
- exit(4);
- }
- cout << "listen success " << endl;
- }
- void Sverse(int fd)
- {
- char buffer[1024];
- while (1)
- {
- // 读取数据
- // tcp是面向字节流和文件管道差不多
- // 直接使用文件的读写方法即可
-
- ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
- if (n > 0)
- {
- // 对同一个fd进行读写
- buffer[n] = 0;
- cout << "client say#" << buffer << endl;
- string echo_string = "client say#";
- echo_string += buffer;
- write(fd, echo_string.c_str(), echo_string.size());
- }
- else if (n == 0) // 如果返回值为0 ,代表读到了文件结尾 (对端关闭了连接)
- {
- cout << "client quit..." << endl;
- break;
- }
- else
- {
- cout << "read Error" << "error code : " << errno << "error string : " << strerror(errno) << endl;
- break;
- }
- }
- }
- void Start()
- {
- // 服务器启动了
- _isrunning = true;
- while (1)
- {
- // 先获取连接
- // 后两个参数为输入输出型参数//等同于UDP————recvfrom
- // 返回值 成功了返回一个非零的新的文件描述符(网络套接字) 失败了返回-1
- //
- struct sockaddr_in peer;
- socklen_t len = sizeof(peer);
- int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
- if (sockfd < 0)
- {
- cout << "accept Error" << "error code: " << errno << "error string: " << strerror(errno) << endl;
- // 监听失败继续监听
- continue;
- }
- cout << "accept success , get a new sockfd : " << sockfd << endl;
-
- // 提供服务
- Sverse(sockfd);
- close(sockfd);
- }
- }
-
- ~TcpServer()
- {
- if (_listensock > 0)
- {
- close(_listensock);
- }
- }
-
- private:
- // 端口号
- uint16_t _port;
- int _listensock;
- bool _isrunning;
- };
Main.cc
- #include<iostream>
- #include<memory>
- #include"TcpServer.hpp"
- using namespace std;
- void Usage(const string & process)
- {
- cout<<"Usage:"<<endl<<process <<" local_port"<<endl;
- }
- int main(int argc ,char * argv[])
- {
- if(argc!=2)
- {
- Usage(argv[0]);
- return 1;
- }
- uint16_t port = stoi(argv[1]);
- auto* tsvr = new TcpServer(port);
- tsvr->Init();
- tsvr->Start();
- delete tsvr;
- return 0;
- }
和服务端的一样
和服务端的一样
connect()
函数用于客户端向服务器发起连接请求。它在客户端程序中使用,用于连接到远程服务器。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值
connect()
函数的返回值是一个整数,表示执行结果的状态:
errno
来指示具体的错误类型。参数
sockfd
:是一个已经通过 socket()
函数创建的套接字描述符,即待连接的套接字。
addr
:是一个指向 struct sockaddr
类型的指针,用于存储远程服务器的地址信息。通常是使用 struct sockaddr_in
或 struct sockaddr_in6
结构体来表示 IPv4 或 IPv6 地址。这个参数包含了远程服务器的 IP 地址和端口号。
addrlen
:是一个 socklen_t
类型的整数,表示 addr
结构体的长度。
注:这个参数调用成功操作系统会自动绑定。
因为socket的本质也是一个文件描述符,因此我们可以像上面一样使用文件的读写方法来发送和收取消息。
- #include <iostream>
- #include <string>
- #include <cstring>
- #include <unistd.h>
- #include <cstdlib>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- using namespace std;
- bool visitserver(string &serverip, uint16_t serverport, int *cnt)
- {
- // 创建套接字
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd < 0)
- {
- cout << "client sockfd error" << endl;
- return false;
- }
- bool ret = true;
- // 客户端不用绑定 , 客户端必须要有服务端的IP和port , 需要绑定,但是不需要用户显示绑定,因为client系统会随机绑定端口
-
- // Tcp发起连接的时候,client会被操作系统自动绑定
-
- // 发起连接
- struct sockaddr_in server;
- memset(&server, 0, sizeof(server));
- server.sin_family = AF_INET;
- server.sin_port = htons(serverport);
- // 字符串的点分十进制转为四字节
- inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
- int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server)); // 自动进行绑定
- // 连接失败
- if (n < 0)
- {
- cout << "connect error" << endl;
- ret = false;
- goto END;
- }
- // 重连成功,重连次数清零
- *cnt = 0;
- // 进行通信
- while (1)
- {
- string inbuffer;
- cout << "Plase Enter";
- getline(cin, inbuffer);
- if (inbuffer == "quit")
- break;
- ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());
- if (n > 0)
- {
- char buffer[1024];
- ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);
- if (m > 0)
- {
- buffer[m] = 0;
- cout << "get a echo message ->" << buffer << endl;
- }
- else if (m == 0)
- {
- break;
- }
- else
- {
- ret = false;
- goto END;
- }
- }
- else
- {
- ret = false;
- goto END;
- }
- }
- END:
- close(sockfd);
- return ret;
- }
- void Usage(const string &process)
- {
- cout << "Usage : " << process << " server_ip server_port" << endl;
- }
- int main(int argc, char *argv[])
- {
- if (argc != 3)
- {
- Usage(argv[0]);
- return 1;
- }
- string serverip = argv[1];
- uint16_t serverport = stoi(argv[2]);
- // 定制重连
- int cnt = 1;
- while (cnt <= 5)
- {
- bool result = visitserver(serverip, serverport, &cnt);
- if (result)
- {
- cnt = 5;
- break;
- }
- else
- {
- sleep(1);
- cout << "server offline, retrying...,cout : " << cnt++ << endl;
- cnt++;
- }
- }
- // 大于重连次数
- if (cnt >= 5)
- {
- cout << "server offline" << endl;
- }
- return 0;
- }
-
- //
uint32_t inet_addr(const char *cp);
这个函数在上篇文章中是用来将点分十进制的字符串IP地址转化为四字节的网络序列的,但是这个函数由于它的实现方法原因是不是线程安全的函数,以后我们尽量使用下面的函数。
int inet_pton(int af, const char *src, void *dst);
返回值
errno
来指示具体的错误类型。参数
af
:指定地址族(Address Family),通常是 AF_INET
(IPv4)或 AF_INET6
(IPv6)。src
:指向以字符串形式表示的 IP 地址的指针,即点分十进制格式的 IP 地址。dst
:指向用于存储结果的缓冲区的指针,通常是一个 struct in_addr
结构体的指针(IPv4)或 struct in6_addr
结构体的指针(IPv6)。今天对网络套接字的分享到这就结束了,希望大家读完后有很大的收获,也可以在评论区点评文章中的内容和分享自己的看法;个人主页还有很多精彩的内容。您三连的支持就是我前进的动力,感谢大家的支持!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。