Linux下基于C++11的socket网络编程(基础)个人总结版

发布于:2025-06-30 ⋅ 阅读:(19) ⋅ 点赞:(0)

跟着这个人做的,感觉是一个非常好的socket入门的代码,而且文件命名也有,代码还全,复制就能跑,对小白非常友好
https://blog.csdn.net/RMB20150321/article/details/121478376?spm=1001.2014.3001.5502
一共五个版本从简单到难,打算和这个博主一样做。
另外,感谢deepseek救我狗命
阅读建议:直接先看2,有函数不懂的看1找。

1.Socket编程中的各种特殊类型/函数

1.1sockaddr_in

作用:用来表示IP地址的结构体(和sockaddr的区别是,sockaddr把目标地址和端口信息混合在一起,不像sockaddr_in是分开的)。但我觉得用IP地址的结构体来描述不妥当,因为很容易认为只和网络层用的IP地址有关系。但实际上它既存储传输层tcp协议要用到的端口号,又存储了网络层IP协议要用的Ip地址。
在定义的文件中,sockaddr_in的定义为

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_); //地址族 表明网络地址的类型和格式 ipv4要写AF_INET,ipv6要写AN_INET6
    in_port_t sin_port;			// 端口号
    struct in_addr sin_addr;	// ip地址

    /* 填充字段,目的是让 sockaddr_in 结构体的大小和通用的 struct sockaddr 结构体大小相同 */
    unsigned char sin_zero[sizeof (struct sockaddr)
			   - __SOCKADDR_COMMON_SIZE
			   - sizeof (in_port_t)
			   - sizeof (struct in_addr)];
  };

1.2socklen_t

用于表示套接字地址结构长度的数据类型
这里存储的是服务器客户端中的网络地址结构的长度,因为网络地址中IPV4和IPV6的地址长度是不同的

1.3socket()

用来生成套接字的
socket函数的定义如下
extern int socket (int __domain, int __type, int __protocol)
extern:该函数的定义在其他文件中(通常是系统库),当前文件只需引用其声明
__domain::指定网络层协议,常见值:AF_INET:IPv4 协议; AF_INET6: IPv6 协议; AF_UNIX:UNIX 域套接字(本地通信)
__type : 指定传输层协议,常见值:SOCK_STREAM:面向连接的 TCP 协议。SOCK_DGRAM:无连接的 UDP 协议。SOCK_RAW:原始套接字(直接访问底层协议)
__protocol:通常为 0,表示使用默认协议(由 __domain 和 __type 决定)
用的时候 int socket_A=socket(AF_INET,SOCK_DGRAM,0);
● 成功:返回非负整数(文件描述符),用于后续操作。
● 失败:返回-1,并设置errno(如EADDRINUSE表示端口已被占用)

1.4 bind

把定义出来的socket和一个特定的 addr关联起来。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符 socket()返回的就是
addr:可以简单理解为sockaddr_in定义的内容,但是要求的是sockaddr所以实际使用的时候还要转换
addrlen :sockaddr_in定义的内容的大小
● 成功:返回0。
● 失败:返回-1,并设置errno(如EADDRINUSE表示端口已被占用)

1.5 listen

开始监听
int listen(int __fd, int __n) throw();
__fd:socket
_n:最多在流程中的数量,注意:是在流程中,不是一共只能连五个。存放未完成连接(正在三次握手)和已完成连接但是还没accept(完成三次握手还没开始准备接收)
● 成功:返回0。
● 失败:返回-1,并设置errno

1.6 accept

用于创建一个新的用于和客户端通信的套接字
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:服务器监听套接字(由 socket() 创建,经 bind() 和 listen() 配置)。
addr:指向客户端地址结构的指针,用于存储客户端的 IP 和端口信息。
addrlen:指向 socklen_t 类型的指针,表示 addr 结构的大小(传入时为初始大小,返回时为实际大小)。

1.7read和write

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, void *buf, size_t count);
fd:文件描述符。
buf:指向缓冲区的指针,用于存储接收到的数据。
count:最多读取的字节数/写入的字节数。
● 成功:返回实际读取的字节数(可能小于 count)。
● 0:表示连接已关闭(客户端主动断开)。
● -1:表示出错,并设置 errno(如 ECONNRESET 表示连接被重置)。

2.代码编写

2.1思路介绍(服务端)

首先,在服务端,我们的目的就是能接收到客户端的信息,然后可以把消息返回。而服务端和客户端的连接是多个客户端连接同一个服务端
具体的步骤:首先服务端要初始化,得先有一个。然后接收客户端的数据,然后处理好后把内容发送回客户端。而为了让一个客户端连接了服务端以后,不阻塞其他的客户端连接,所以我们需要把接收客户端请求和接收客户端数据分开来,一个用来监听客户端的请求(函数1),另一个负责具体连接接收数据发送数据之类的(函数2)。接收客户端请求的方法(函数1)是一直监控服务端的端口,看有没有人连接,一旦有人连接交给(函数2)进行。连接是TCP连接,两端都要有插口(socket)然后连接。这样服务端+客户端一共会定义三个socket:服务端两个(一个用来监听自己的端口,一个用来和客户端做连接),客户端一个(用来和服务端做连接)。一切结束后,资源还需要释放。
所以服务端现在需要的函数
初始化: InitServer(const string& port); //port:端口号
监听/接收请求:Accept();
接收客户端数据:Read();
把数据发回给客户端:Write();
资源释放:关闭服务端监听自己的:CloseServerSock();
关闭客户端连接的:CloseClientSock();
在这里插入图片描述

private:
    struct sockaddr_in serv_addr;//服务器中服务端的网络地址结构
    struct sockaddr_in clnt_addr;//服务端中客户端的网络地址结构
    socklen_t clnt_addr_len;//服务器中客户端的网络地址结构的长度
    char buf[BUFF_SIZE];//读写缓冲区
    int buf_len;//读取的字节长度
    int serv_sock;//服务器中服务端的socket描述符
    int clnt_sock;//服务器中客户端的socket描述符

上面的buf[BUFF_SIZE]和buf_len
服务端接收数据先放到缓冲区里,然后读。要发送时,先把要发的内容写到缓冲区里然后通过缓冲区发送。
另外由于是单线程所以服务端现在只有一个缓冲区。我们现在就是测试一下他的通信功能,多客户端后面进行改写。

2.2函数

//构造函数
//-1表示服务还没开启,
TcpServer::TcpServer():serv_sock(-1),clnt_sock(-1){}
//析构函数
TcpServer::~TcpServer(){}

2.2.1InitServer(const string& port); //port:端口号

实现上图中socket,bind,listen

//实现socket
bool TcpServer::InitServer(const string& port) {//port是端口 
    serv_sock=socket(AF_INET, SOCK_STREAM,0);
    if(serv_sock==-1){
        //endl 相当于换行+强制刷新
        cout <<"服务器创建套接字失败" <<endl;
        return false;
    }
    // 将&serv_addr开始的位置,字节覆盖为0,覆盖大小为serv_addr
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(stoi(port));
}
// 上面这些运行完,我们就定义了一个完整的socket
// 其中网络层用IPv4协议、传输层用tcp协议,其余的使用默认协议
// 监听所有的IP地址,端口绑定为输入的port

htonl(INADDR_ANY)和htons(stoi(port))的解释
htonl/s:host to network long/short 把长的(32位)/短的(16位)主机字节序转换成网络字节序
也可以叫 把小端序转成大端序。小端序低位字节存于低地址,大端序低位字节存于高地址,网络使用大端序,PC和服务器使用小端序。所以需要用这两个函数进行转换
INADDR_ANY:值为0,表示监听服务器的所有可用 IP 地址,这里的意思是,假设服务器有多个地址:公网、局域网、回环。客户端连服务器的哪个地址都可以
stoi( x):把字符串x转换成整数

//实现bind
if(bind(serv_sock , (struct sockaddr*)& serv_addr , sizeof(serv_addr)) == -1){
		cout<<"服务器绑定套接字失败,bind() error!"<<endl;
		//关闭服务端的socket
		CloseServerSock();
		return false;
 
}
//实现listen
if(listen(serv_sock,1)==-1){
    cout<<"监听失败"<<endl;
    CloseServerSock();
    return false;
}
// 整体代码
bool TcpServer::InitServer(const string& port){
    // 如果在开启的时候发现服务之前已经开启了,需要先关闭再重新开启
    // 原因:避免两个客户端连到同一个socket上,导致后一个连接失败。
    if(serv_sock>0){
        CloseServerSock();
        serv_sock=-1;
    }
    // 定义socket框架
    serv_sock=socket(AF_INET, SOCK_STREAM,0);
    if(serv_sock==-1){
        cout <<"服务器创建套接字失败" <<endl;
        return false;
    }
    // 定义具体内容
    // 将&serv_addr开始的位置,字节覆盖为0,覆盖大小为serv_addr
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(stoi(port));
    // 把具体内容绑定到框架上
    if(bind(serv_sock , (struct sockaddr*)& serv_addr , sizeof(serv_addr)) == -1){
		cout<<"服务器绑定套接字失败,bind() error!"<<endl;
		//关闭服务端的socket
		CloseServerSock();
		return false;
    }
    // 进行端口的监听
    if(listen(serv_sock,5)==-1){
    cout<<"监听失败"<<endl;
    CloseServerSock();
    return false;
    }
    cout <<"服务端初始化完成,开始接收客户端连接" << endl;
    return true;
}

2.2.2Accept();

思路:先看服务端socket有没有初始化完成,然后把监听socket的信息拿来,经由accept函数创建出一个新的负责与特定客户端通信的socket
为什么serv_sock需要结构体初始化,clnt_sock就不用了呢?
因为clnt_sock里面的内容是serv_sock接收到客户端传来的内容生成的。在编写客户端代码时可以看到客户端用clnt_sock的时候就需要结构体初始化了。

bool TcpServer::Accept{
    if(serv_sock==-1){
        cout <<"服务器监听端口初始化失败"<< endl;
        return false;
    }
    clnt_addr_len=clnt_addr.size();
    clnt_sock=accept(serv_sock,(struct sockaddr*)& clnt_addr,clnt_addr_len);
    if(clnt_sock==-1){
        cout<<"创建失败"<<endl;
        return false;
    }
    return true;
}

2.2.3Read();

读客户端发送的内容,就是读clnt_sock对应的缓冲池的内容

int TcpServer::Read(){
    if(clnt_sock==-1){
        cout << "没有客户端连接"<< endl;
        return 0;
    }
    buf_len=read(clnt_sock,buf,BUFF_SIZE-1);
    if(buf_len==0){
        CloseClientSock();
        return 0;
    }
    buf[buf_len] = '\0';
	cout<<"【"<<clnt_sock<<"】说:"<<buf<<endl;
	return buf_len;
}

buf[buf_len] = ‘\0’; 许多字符串函数依赖 ‘\0’ 判断字符串结束: 而buf_len = read(…)从套接字读取原始字节数据,buf_len 是实际读取的字节数(不含 ‘\0’)。

2.2.4Write();

void TcpServer::Write(){
    if(clnt_sock==-1{
        cout <<"无客户端连接,写入失败"<<endl;
    }
    else{
        // 读到什么就原样返回什么
        write(clnt_sock,buf,buf_len);
    }
}

2.2.5CloseServerSock(); CloseClientSock();

非常简单
serv socket或者client socket大于0了就用close关掉(防止资源泄漏?)然后赋值为-1

void TcpServer::CloseServerSock(){
	if(serv_sock > 0){
		close(serv_sock);
		serv_sock = -1;
	}
}
 
void TcpServer::CloseClientSock(){
	if(clnt_sock > 0){
		close(clnt_sock);
		clnt_sock = -1;
	}
}

2.3思路(客户端)

和服务端很像,唯一的区别是它没有监视的端口,所以socket、结构体都只定义一个就可以了
比服务器多了的内容是它需要找到服务器。
服务器是不需要找客户端的,它只要等着别人连他就行。但是客户端需要主动去连服务器,所以会增加连接功能。
先把和服务端差不多的代码写好

#include "TcpClient.h"
 
TcpClient::TcpServer():clnt_sock(-1),buf_len(0){
	cout<<"TCP客户端的构造函数"<<endl;
}
 
TcpClient::~TcpServer(){
	cout<<"TCP客户端的析构函数"<<endl;
}
 
bool TcpClient::InitClient(){
 
	//先判断服务是否已经开启
	if(clnt_sock > 0){
		CloseServerSock();
		clnt_sock = -1;
	}
 
	//创建服务器中的服务端socket
	clnt_sock = socket(AF_INET , SOCK_STREAM , 0);
	if(clnt_sock == -1){
		cout<<"客户端创建套接字失败,socket() error!"<<endl;
		return false;
	}	

	cout<<"客户端创建套接字成功!"<<endl;
	return true;
}

void TcpClient::Write(const string& str){
	if(clnt_sock == -1){
		cout<<"未有客户端进行连接,写入失败"<<endl;
	}else{
		write(clnt_sock , str.c_str() , str.size());
	}
}
 
int TcpClient::Read(){
	if(clnt_sock == -1){
		cout<<"客户端未开启socket"<<endl;
		return 0;
	}
	buf_len = read(clnt_sock , buf , BUFF_SIZE-1);
	if(buf_len == 0){
		//对端断开了连接,关闭对端
		CloseClientSock();
		return buf_len;
	}
	buf[buf_len] ='\0';
	cout<<"【服务器】说:"<<buf<<endl;
	return buf_len;
}
 
void TcpClient::CloseClientSock(){
	if(clnt_sock > 0){
		close(clnt_sock);
		clnt_sock = -1;
	}
}

多的内容是连接服务器,因此定义一个connect函数,里面包含connect方法。这里额外说一下
客户端需要额外指定的是服务端的ip和端口号,服务端需要额外指定的也是服务端的ip和端口号。客户端的ip和端口号在客户端用connect()自动获取,在服务端用accpet()获取

bool TcpClient::Connect(const string& ip_addr,const string& port){
    //和server端是一样的
    if(clnt_sock==-1){
        cout <<"客户端没有创建socket,无法连接服务器" <<endl;
        return false;
    }
    memset(&clnt_sock,0,sizeof(clnt_sock));
    clnt_sock.sin_family=AF_INET;
    // 这里不能像server一样 监听所有自己的IP地址了
    // clnt_sock.sin_addr.s_addr=htonl(INADDR_ANY);
    // 客户端需要指定服务端的IP地址和端口号
    clnt_sock.sin_addr=GetHostByName(ip_addr);
    clnt_sock.sin_port=htons(stoi(port));
    // 和server的accept类似,唯一区别是最后一个参数不用传指针
    if(connect(clnt_sock , (struct sockaddr*)& clnt_addr , sizeof(clnt_addr)) == -1){
        CloseClientSock();
        cout<<"连接服务器失败"<<endl;
        return false;
    }
    cout<<"连接服务器成功..."<<endl;
    return true;
}

gethost函数主要解决的问题是将输入的地址,不管这个地址是域名还是IP地址都转成二进制格式的,可以赋值给clnt_sock.sin_addr的。
如果只传递ip地址其实没必要写一个额外的函数可以直接把
clnt_addr.sin_addr = GetHostByName(ip_addr);
替换成
inet_pton(AF_INET, ip_addr.c_str(), &clnt_addr.sin_addr)

in_addr GetHostByName(const string& ip_addr){
    struct hostent* host;
    // 用来解析域名的
    host = gethostbyname(ip_addr.c_str());
    struct in_addr _sin_addr;
    if(!host){
        return _sin_addr;
    }
    _sin_addr = (*(struct in_addr*)host->h_addr_list[0]);
    return _sin_addr
}

3.main函数

3.1服务端

//argc(argument count):表示参数的数量,包括程序名本身。
//argv(argument vector):是一个字符串数组(每个元素是 char* 类型),存储具体的参数值。
// 比如执行./server 8080时,argc会变成2(两个参数),argv[0]=“/server”,argv[1]="8080" 
int main(int argc ,char* argv[]){
    if(argc!=2){// 说明没输入端口号
        cout<<"端口号不对"<<endl;
        return -1;
    }
    TcpServer tcp_server;
    //初始化服务器
    bool init_res=tcp_server.InitServer(argv[1]);
    if(!init_res){
        cout<<"服务器初始化失败"<<endl;
        return -1;
    }
    //时刻想着最前面那张图,初始化之后要监控
    bool accept_res=tcp_server.Accept();
    if(!accept_res){
        cout<<"服务器无法接受客户端的连接"<<endl;
        return -1;
    }
    // 然后开始read 和write,客户端没说停不能停
    while(1){
        // 这个read返回0两个条件
        //1.缓冲区内容已空(每次读取完都会空)
        //2.接收到了客户端发的FIN包。
        //如果没接到FIN包,且缓冲区为空,read函数不会返回值,它会一直卡在那儿直到有值!
        if(tcp_server.Read()==0){
            cout<<"客户端退出了"<<endl;
            break;
        }
        //开始回声:来了的原样传回去
        tcp_server.Wirte();
    }
    // 释放资源
    tcp_server.CloseServerSock();
    tcp_server.CloseClientSock();
    return 0}

3.2客户端

// 127.0.0.1 8080
int main(int argc ,char* argv[]){
    if(argc!=2){// 说明没输入端口号
        cout<<"端口号不对"<<endl;
        return -1;
    }
    TcpServer tcp_server;
    //初始化客户端
    bool init_res=tcp_client.InitClinent();
    if(!init_res){
        cout<<"客户端初始化失败"<<endl;
        return -1;
    }
    //时刻想着最前面那张图,初始化之后要连接
    bool connect_res=tcp_clinet.Connect(argv[1],argv[2]);
    if(!connect_res){
        cout<<"连接失败"<<endl;
        return -1;
    }
    // 然后开始read 和write,设置一个停止条件
    cout<<"开始发送数据(输入q退出)"<<endl;
    while(1){
        string str;
        cin >>str;
        tcp_client.Write(str);
        if(str=="q"){
            break;
        }
        if(tcp_client.Read()==0){
            cout<<"客户端退出了"<<endl;
            break;
        }
    }
    // 释放资源
    tcp_client.CloseClientSock();
    return 0}

可以看到这个代码其实是有问题的。
第一个问题:现在服务器只能监听一个线程。
第二个问题:
按现在这个代码来看。首先:客户端会认为每当读到一个空格代表我的一个输入结束。
所以当输入"ni hao wo shi ",客户端会默认这是四句话发送给服务端,然后服务端分别返回。这个问题还好,可以接受。
但如果客户端输入的内容超过应用程序的缓冲区大小,客户端接收到的内容就会乱掉,即延迟回写(自己运行看),因为客户端只能读一次,读完一次它就等着用户写了。即使还有内核接收缓冲区没读完,它也不读了。
一开始我不想大改代码,能正确的发送什么返回什么就行。因此一开始的思路是,一直读内核接收缓冲区的内容放到应用程序的缓冲区里,直到应用程序缓冲区里没有内容了,我再开启客户端的write()和服务端通信。还是那句话,能用就行
代码如下:

#include <sys/ioctl.h>
int TcpClient::Read(){
    if(clnt_sock == -1){
        cout<<"客户端未开启socket"<<endl;
        return 0;
    }
    int bytes_available;
    while(true){
        buf_len = read(clnt_sock , buf , BUFF_SIZE-1);
        if(buf_len == 0){
            //对端断开了连接,关闭对端
            CloseClientSock();
            return buf_len;
        }
        buf[buf_len] ='\0';
        cout<<"【服务器】说:"<<buf<<endl;
        if(ioctl(clnt_sock, FIONREAD, &bytes_available) < 0) {
            cout<<"ioctl 失败"<<endl;
            return 0;
        }
        // 如果没有数据可读,退出循环
        if(bytes_available <= 0) {
            break;
        }
    }
    return buf_len;
}

ioctl(clnt_sock, FIONREAD, &bytes_available)

  1. 查询内核中与 clnt_sock 关联的接收缓冲区
  2. FIONREAD:控制命令,指定要执行的操作,不同的设备类型有不同的命令集。
  3. 获取缓冲区中等待被读取的字节数
  4. 将这个数字存储在 bytes_available 变量中。
    但是运行还是会出问题
    在这里插入图片描述
    每次返回的内容可能不全。下面上八股解释:

TCP通信中的数据流动过程

客户端写入的时候和定义的缓冲区大小无关,通过指向数据的指针,直接将数据写到客户端内核发送缓冲区中。如果不满足Nagle条件就直接发送,满足就延迟发送。
注:当以下两个条件同时满足时,Nagle 算法会触发「数据缓存合并」,而非立即发送:
undefined 存在未被确认的字节(即之前发送的数据尚未收到对方的 ACK 确认);
undefined 待发送的数据量小于 MSS(最大段大小)。
如果延迟发送,那就涉及到了粘包的问题。因为TCP传输的是无边界的字节流。不知道不同的消息怎么分。
为了解决TCP出现的问题,我们在应用层去解决它。因此:解决粘包的三种方式

(1)发送定长包。即发送端将每个数据包封装为固定长度(长度不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。(适合定长结构的数据)
(2)包头加上包体长度。发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便可以知道每一个数据包的实际长度了。(适合不定长结构的数据)
(3)在包尾部设置边界标记。发送端在每个数据包尾部添加边界标记,可以使用特殊符号作为边界标记。如此,接收端通过这个边界标记就可以将不同的数据包拆分开来。但这可能会存在一个问题:如果数据包内容中也包含有边界标记,则会被误判为消息的边界,导致出错。这样方法要视具体情况而定。例如,FTP协议就是采用 “\r\n” 来识别一个消息的边界的。

发送的路径是从客户端内核的发送缓冲区到服务端内核的接收缓冲区。由于服务端应用程序的缓冲区比较小所以它会读好几次,读完就写到服务端内核发送缓冲区中,直到内核接收缓冲区为空才会停止读。
然后信息从服务端内核发送缓冲区到客户端内核接收缓冲区。客户端也会一直读内核接收缓冲区直到它为空。
但是注意Nagle 算法触发「数据缓存合并」,服务端发送的时候可能之前发送的内容没收到确认,所以客户端内核接收缓冲区会出现短暂的空白->应用程序缓冲区空白->break->接着请求写。这时上一次的数据可能已经到客户端内核接收缓冲区了,但是没有用了。怎么解决?就是我们之前提到的:解决粘包的三种方式。这里我们使用包头+包体长度的方法
服务端和客户端改得都是读写的方式,在包头添加这次输入的长度。根据长度来判断要不要合并发回。读写逻辑基本一样。

服务端

int TcpClient::Read(){
    if(clnt_sock == -1){
        cout<<"客户端未开启socket"<<endl;
        return 0;
    }
    uint32_t net_len;
    buf_len=read(clnt_sock,&net_len,sizeof(net_len));
    if (buf_len<=0) return buf_len;
    // 转换为主机字节序
    uint32_t msg_len = ntohl(net_len);
    int total_body_bytes = 0;
    string result="";
    while(total_body_bytes < msg_len){
        int to_read=std::min(msg_len-total_body_bytes,static_cast<uint32_t>(BUFF_SIZE-1));
        buf_len = read(clnt_sock , buf , to_read);
        result.append(buf,buf_len);
        total_body_bytes+=buf_len;
    }
    cout<<"【服务器】说:"<<result<<endl;
    return buf_len;
}

void TcpClient::Write(const string& str){
    if(clnt_sock < 0){
        cout<<"客户端未开启socket,写入失败!"<<endl;
        return;
    }
    int content_len=str.size();
    uint32_t net_len=htonl(content_len);// 转换成网络字节序
    std::vector<char> packet(sizeof(net_len)+content_len);
    // 把包头和内容全都拷贝到packet中。
    memcpy(packet.data(),&net_len,sizeof(net_len));
    memcpy(packet.data()+sizeof(net_len),str.data(),content_len);
    std::string strs(packet.begin(), packet.end());
    std::string str1(packet.begin(), packet.end());
    write(clnt_sock,packet.data(),packet.size());
}

客户端

string TcpServer::Read(){
	if(clnt_sock == -1){
		cout<<"未有客户端进行连接"<<endl;
		return 0;
	}
	string result="";
	uint32_t net_len;
	buf_len=read(clnt_sock,&net_len,sizeof(net_len));
	if (buf_len<=0) return result;
	// 转换为主机字节序
	uint32_t msg_len = ntohl(net_len);
	int total_body_bytes = 0;
	while(total_body_bytes < msg_len&&buf_len!=0){
		int to_read=std::min(msg_len-total_body_bytes,static_cast<uint32_t>(BUFF_SIZE-1));
		// 清零缓冲区
		memset(buf, 0, BUFF_SIZE);
		buf_len = read(clnt_sock , buf , to_read);
		if(buf_len <= 0) break;
		//cout<<"【"<<clnt_sock<<"】说:"<<buf<<endl;
		result.append(buf, buf_len);
		total_body_bytes+=buf_len;
	}
	if(buf_len == 0){
		//对端断开了连接,关闭对端
		CloseClientSock();
		result="";
	}
	cout<<"【"<<clnt_sock<<"】说:"<<result<<endl;
	return result;
}
 
void TcpServer::Write(const string& str){
	if(clnt_sock < 0){
		cout<<"客户端未开启socket,写入失败!"<<endl;
		return ;
	}
	int content_len=str.size();
	uint32_t net_len=htonl(content_len);// 转换成网络字节序
	std::vector<char> packet(sizeof(net_len)+content_len);
	// 把包头和内容全都拷贝到packet中。
	memcpy(packet.data(),&net_len,sizeof(net_len));
	memcpy(packet.data()+sizeof(net_len),str.data(),content_len);
	write(clnt_sock,packet.data(),packet.size());
	return;
}

uint32_t net_len=htonl(content_len);// 转换成网络字节序
这里别忘了包体长度也要转换成网络字节序,可能是一种约定俗成?

那么到这儿实现的就是最最简单的单线程的阻塞IO模型通信:在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞,不能处理别的网络IO。

本来是想学分布式的,发现学分布式要学raft,学raft要学rpc,学rpc要会socket。好家伙直接要从盘古开天开始学。要的很急开始压力自己两天跑完五个版本,谁能想到我会基础差成这个样子。内容我用语雀写了直接粘过来的,代码不太全,不过思路十分全。希望对后面学的人有帮助吧。最最简单的内容竟然做了两天,socket编程确实是一无所知,我之前的一年到底在做什么啊,orz。立个flag剩下的四个版本一周看完。