套接字(Socket)是计算机网络中实现网络通信的一种编程接口。它提供了应用程序与网络通信之间的一座桥梁,因为它允许应用程序通过网络发送和接收相应的数据以实现不同主机之间的通信。
通常套接字由以下两部分组成:
1.网络IP和端口号:IP用来标识主机,而端口号可以标识到单台主机的唯一进程。
2.通信协议:套接字通过规定通信协议来制定数据传输和发送的规则。常见的有TCP和UDP等协议。
TCP是一种面向连接的协议,提供可靠的、有序的、基于字节流的数据传输。
UDP是一种无连接的协议,提供不可靠的、无序的、基于数据报的数据传输。
今天我们来学习TCP实现网络通信。TCP由于能提供可靠、基于字节流的数据传输,使用率与使用场景也比UDP多很多。
我们来看看能实现出什么样的结果(聊天室模拟两个用户随机通信):
若不开启服务端就只开启客户端的话,那么就会像打游戏的某些情况连不上:
当服务端和客户端都开启后就可以正常通信了:
这里我们还是通过客户端给服务器端发送消息,通过TCP链接实现通信。
那么事不宜迟,我们马上开始分享实现过程吧!
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
:自动选择合适的协议 在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位无符号整数
listen()函数:
将套接字设置为监听状态,等待连接请求。
参数:
accept()
函数:接受客户端的连接请求,创建一个新的套接字用于与客户端进行通信。
参数:
connect()
函数:发起与远程主机建立TCP连接的请求。
参数:
在整个服务器端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;
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 " < 0) //代表成功读取
{
buffer[s] = 0;
std::string res = _func(buffer); //回调显示
std::cout<< res <
在服务端的主文件中,我们直接包含上面的头文件。
我们期望的用法是:./tcp_server port,代表运行可执行文件后面需要带一个参数:端口号
所以我们能够从用户中输入的port,通过main函数中的**char* argv[]**参数列表中获取到。并传给tcpSercer类中初始化与启动服务器即可。
#include "server.hpp"
#include
using namespace std;
static void usage(string proc) //使用手册,代表运行可执行文件后面需要带一个参数:端口号
{
std::cout << "Usage:\n\t" << proc << "port\n" < ts(new tcpServer(echo,port));//采用智能指针创建释放资源
ts->InitServer();
ts->Start();
return 0;
}
在客户端中我们conncet尝试链接到服务器端,这里需要做一个重连反馈:正在尝试重连…
我们期望运行格式:./client serverip serverport,代表运行可执行文件后需要两个参数:IP和端口
我们从用户输入的两个参数中传给main,并通过main参数char* argv[]参数列表获取到,之后获取到直接转化即可。
static void usage(string proc)
{
std::cout << "Usage:\n\t" << proc << "serverip serverport\n" <> ";
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 >>>" <
最后运行之后就能获得我们之前通信的结果了: