Linux下套接字TCP实现网络通信
文章目录
- Linux下套接字TCP实现网络通信
- 1.引言
- 2.具体实现
- 2.1接口介绍
- 1.socket()
- 2.bind()
- 3.listen()
- 4.accept()
- 5.connect()
- 2.2 服务器端server.hpp
- 2.3服务端server.cc
- 2.4客户端client.cc
1.引言
套接字(Socket)是计算机网络中实现网络通信的一种编程接口。它提供了应用程序与网络通信之间的一座桥梁,因为它允许应用程序通过网络发送和接收相应的数据以实现不同主机之间的通信。
通常套接字由以下两部分组成:
1.网络IP和端口号:IP用来标识主机,而端口号可以标识到单台主机的唯一进程。
2.通信协议:套接字通过规定通信协议来制定数据传输和发送的规则。常见的有TCP和UDP等协议。
TCP是一种面向连接的协议,提供可靠的、有序的、基于字节流的数据传输。
UDP是一种无连接的协议,提供不可靠的、无序的、基于数据报的数据传输。
今天我们来学习TCP实现网络通信。TCP由于能提供可靠、基于字节流的数据传输,使用率与使用场景也比UDP多很多。
我们来看看能实现出什么样的结果(聊天室模拟两个用户随机通信):
若不开启服务端就只开启客户端的话,那么就会像打游戏的某些情况连不上:
当服务端和客户端都开启后就可以正常通信了:
这里我们还是通过客户端给服务器端发送消息,通过TCP链接实现通信。
那么事不宜迟,我们马上开始分享实现过程吧!
2.具体实现
2.1接口介绍
1.socket()
socket函数是用于创建套接字的函数,创建成功返回文件描述符fd,失败返回-1;
int socket(int domain, int type, int protocol);
参数说明:
-
domain
:指定套接字的地址族(Address Family)
今天我们选择:
AF_INET
:IPv4 地址族
-
type
:指定套接字的类型(Socket Type)
今天我们选择:
SOCK_STREAM
:有连接的字节流套接字,用于TCP协议
-
protocol
:可选参数,指定具体的传输协议。常用的有:
今天我们选择:
0
:自动选择合适的协议
2.bind()
在Linux下,bind()
函数用于将一个套接字(socket)与特定的IP地址和端口号进行绑定。
*int bind(int sockfd, const struct sockaddr addr,socklen_t addrlen);
参数说明:
sockfd
:要进行绑定的套接字的文件描述符。addr
:指向一个struct sockaddr
结构体的指针,其中包含要绑定的IP地址和端口号信息。addrlen
:addr
结构体的长度。
在绑定bind的第二个参数中,我们也需要用到库中定义好的sockaddr_in结构体来初始化!
具体结构体struct sockaddr_in说明:
结构体中有三个值也需要初始化指定一下:
sin_family
:表示地址族(Address Family),一般为AF_INET
。
sin_port
:表示端口号。它是一个 16 位的整数,使用网络字节序(大端字节序)表示。在使用时,通常需要使用htons()
函数将主机字节序转换为网络字节序。
sin_addr
:表示 IPv4 地址。它是一个struct in_addr
类型的结构体,用于存储 32 位的 IPv4 地址。一般服务端用INADDR_ANY,让udp_server在启动时候可以绑定任何ip.
客户端用inet_addr函数将字符串转化成32位无符号整数
3.listen()
listen()函数:
将套接字设置为监听状态,等待连接请求。
参数:
- sockfd:套接字的文件描述符。这里我们选择前面socket创建好的返回值.
- backlog:指定等待连接队列的最大长度。一般不会太大,我们这里写32即可。
4.accept()
accept()
函数:接受客户端的连接请求,创建一个新的套接字用于与客户端进行通信。
参数:
- sockfd:套接字的文件描述符。这里我们选择前面socket创建好的返回值.
- addr:指向客户端地址的结构体指针。创建一个sockaddr的结构体强转一下(struct sockaddr*)即可。
- addrlen:客户端地址结构体的字节大小。创建一个socklen_t类型的值用来计算结构体大小。
5.connect()
connect()
函数:发起与远程主机建立TCP连接的请求。
参数:
- sockfd:套接字的文件描述符。这里我们选择前面socket创建好的返回值.
- addr:指向远程主机地址的结构体指针。创建一个sockaddr的结构体强转一下(struct sockaddr*)即可。
- addrlen:远程主机地址结构体的字节大小。创建一个socklen_t类型的值用来计算结构体大小。
2.2 服务器端server.hpp
在整个服务器端server.hpp中,我们需要创建tcpServer类,并在类中建立这些成员:监听套接字、端口号等.
在类中我们还需要初始化服务器InitServer()和启动服务器Start()两个接口。
服务器端具体实现思路是:我们创建套接字socket()后开始绑定bind(),之后监听listen(),监听成功后我们获取链接accept()即可。
思路简单,但是实现还需要很多事完成:
static const uint16_t defaultport = 8081;
static const int backlog = 32;
using func_t = std::function<std::string(const std::string&)>;
class tcpServer
{
public:
tcpServer(func_t func,uint16_t port = defaultport)
:_func(func)
,_port(port)
,_quit(true)
{}
~tcpServer() {}
void InitServer()
{
//1.创建套接字
_listensock = socket(AF_INET,SOCK_STREAM,0);
if(_listensock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
//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 = htonl(INADDR_ANY);
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cerr << "bind error" << std::endl;
exit(-2);
}
//3.监听(tcp)
if(listen(_listensock,backlog) < 0)
{
std::cerr <<" listen error" << std::endl;
exit(-3);
}
}
void Start()
{
_quit = false; //运行时设置位运行状态,即不退出的状态
while(!_quit) //服务器死循环
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
//4.获取链接accept
int sock = accept(_listensock,(struct sockaddr*)&client,&len);
if(sock < 0) {
std::cerr <<"accept error " <<std::endl;
continue;} //揽客的sock失败后继续即可
//5.获取链接成功
std::cout<< "获取链接成功" << sock << " from " << _listensock << std::endl;
service(sock);
}
}
void service(int sock) //服务
{
char buffer[1024];
while(true)
{
ssize_t s = read(sock,buffer,sizeof(buffer)-1);
if(s > 0) //代表成功读取
{
buffer[s] = 0;
std::string res = _func(buffer); //回调显示
std::cout<< res <<std::endl;
write(sock,res.c_str(),res.size());
}
else if(s == 0) //代表读到文件结尾 在网络中就相当于对方关闭链接
{
close(sock);
std::cout << "quit" <<std::endl;
break;
}
else //文件读取失败
{
close(sock);
std::cerr << " read error" <<std::endl;
break;
}
}
}
private:
uint16_t _port; //端口号
int _listensock; //监听套接字
bool _quit; //代表服务器没有运行的状态
func_t _func; //回调包装器,为了后面输出后回显
};
2.3服务端server.cc
在服务端的主文件中,我们直接包含上面的头文件。
我们期望的用法是:./tcp_server port,代表运行可执行文件后面需要带一个参数:端口号
所以我们能够从用户中输入的port,通过main函数中的**char* argv[]**参数列表中获取到。并传给tcpSercer类中初始化与启动服务器即可。
#include "server.hpp"
#include<memory>
using namespace std;
static void usage(string proc) //使用手册,代表运行可执行文件后面需要带一个参数:端口号
{
std::cout << "Usage:\n\t" << proc << "port\n" <<std::endl;
}
std::string echo(const std::string& message)//输出回显
{
return message;
}
//期望用法:./tcp_server port
int main(int argc,char* argv[])
{
if(argc != 2) //输入的不是两个参数,说明你不会用。输出使用手册
{
usage(argv[0]);
exit(-1);
}
uint16_t port = atoi(argv[1]); //强转成能够使用的类型
unique_ptr<tcpServer> ts(new tcpServer(echo,port));//采用智能指针创建释放资源
ts->InitServer();
ts->Start();
return 0;
}
2.4客户端client.cc
在客户端中我们conncet尝试链接到服务器端,这里需要做一个重连反馈:正在尝试重连…
我们期望运行格式:./client serverip serverport,代表运行可执行文件后需要两个参数:IP和端口
我们从用户输入的两个参数中传给main,并通过main参数char* argv[]参数列表获取到,之后获取到直接转化即可。
static void usage(string proc)
{
std::cout << "Usage:\n\t" << proc << "serverip serverport\n" <<std::endl;
}
//期望使用:./client serverip serverport
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(-2);
}
string serverip = argv[1]; //获取到参数
uint16_t port = atoi(argv[2]);
//1.创捷套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
std::cerr << " socket error" <<std::endl;
exit(-1);
}
//2.客户端需要链接服务器 --connect
struct sockaddr_in server; //
memset(&server,0,sizeof(server)); //清空结构体
server.sin_family = AF_INET;//初始化结构体
server.sin_port = htons(port);
//server.sin_addr.s_addr = inet_addr(serverip.c_str()); //客户端
inet_aton(serverip.c_str(),&(server.sin_addr));
int cnt = 5;
while(connect(sock,(struct sockaddr*)&server,sizeof(server)) != 0) //如果绑定失败
{
sleep(1);
std::cout<<"正在尝试重连... 重连次数:" <<cnt-- <<std::endl;
if(cnt <= 0) break;
}
if(cnt <= 0)
{
cerr<< "服务器连接失败"<<endl;
exit(-1);
}
//3.连接成功
while(true) //连接成功后从客户端直接输入发送数据
{
string line;
char buffer[1024];
cout<<"Enter>> ";
getline(cin,line);
write(sock,line.c_str(),line.size()); //给缓冲区写数据
ssize_t s = read(sock,buffer,sizeof(buffer) -1);
if(s > 0)//正常写
{
buffer[s] = 0;
cout<< " server rcho >>>" <<buffer <<endl;
}
else if(s == 0) //写结束
{
cerr << "server quit" <<endl;
break;
}
else{ //异常
cerr<< " read error " <<endl;
break;
}
}
close(sock);//关闭套接字,管不管都可以
return 0;
}
最后运行之后就能获得我们之前通信的结果了: