日期:2025.2.4(凌晨) 2025.2.5(凌晨)
学习内容:
- 网络编程-客户端
- 网络编程-服务端
- 各自的封装
个人总结:
首先这里说一声,在这之间学了个线程池的实现和进程里面信号量的实现,封装的内容,但是由于内容过多,加上学这两个东西的时候查的东西有点多,写出来好麻烦,所以欠的这两篇以后会补上的。不会鸽的。
客户端:
首先先贴上代码,然后依次说明各个部分的内容:
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
using std::cout;
using std::endl;int main(int argc, char* argv[]) {if (argc != 3) {cout << "USING: ./demo1 192.168.11.132 5005" << endl;return -1;}int sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd == -1) {perror("socket");return -1;}struct hostent* k_host;if ((k_host = gethostbyname(argv[1])) == 0) {cout << "gethostbyname failed" << endl;close(sock_fd);return -1;}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(atoi(argv[2]));memcpy(&serv_addr.sin_addr, k_host->h_addr_list[0], k_host->h_length);if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("connect");close(sock_fd);return -1;}char buf[1024];for (int i = 0; i < 3; i++) {int i_res;memset(buf, 0, sizeof buf);sprintf(buf, "%d", i);if ((i_res = send(sock_fd, buf, strlen(buf), 0)) <= 0) {perror("send");break;}cout << "send: " << buf << endl;memset(buf, 0, sizeof buf);if ((i_res = recv(sock_fd, buf, sizeof buf, 0)) <= 0) {perror("recv");break;}cout << "recv: " << buf << endl;sleep(1);}close(sock_fd);return 0;
}
第一部分:
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd == -1) {perror("socket");return -1;}
这一部分是设置一个套接字的描述符。
我们先明确一个大的方向,就是在网络连接中,我们与服务器之间的连接是通过套接字连接的,服务器有一个套接字,我们这里也需要有一个套接字,套接字和套接字连接上就可以进行连接。所以这一步就是用来进行创建套接字,为后面连接做准备。
这里socket的参数基本都是固定的,因为我们大多数都是使用ipv4协议,tcp协议,如果是要做音视频通话之类的,我们一般就是用udp协议。第二个参数就写SOCK_DGRAM
前两个参数分别代表着ipv4和tcp,第三个参数填0是交给编译器自动填值,我们可以默认填0就好了。
第二部分:
struct hostent* k_host;if ((k_host = gethostbyname(argv[1])) == 0) {cout << "gethostbyname failed" << endl;close(sock_fd);return -1;}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(atoi(argv[2]));memcpy(&serv_addr.sin_addr, k_host->h_addr_list[0], k_host->h_length);
看到了serv_addr没有,这个是我们第二部分的主角。
在网络连接中,只有接口是不够的,作为客户端的我们,还需要知道服务端的ip地址,网络协议(其实绝大部分都是ipv4),端口。
一开始有一个k_host,这个是用来为后面设置serv_addr的ip地址做准备的。
中间我们用到一个叫gethostbyname的函数,这个函数是将域名换成ip地址的。
实际上gethostbyname
既可以处理域名,也可以处理点分十进制的 IP 地址字符串。
我们会在运行的时候输入命令
./demo1 192.168.11.132 5005
这里的ip地址和端口都是我预先设置好的,端口这个我用的虚拟机是ubuntu,百度搜一下ubuntu如何开启防火墙的端口就好了。自己设置一个端口就可以。
(k_host = gethostbyname(argv[1]))
这个k_host是个结构体,结构体名字是hostent,里面有很多的内容,我们了解里面有个成员是h_addr_list就好了,这个成员里面存储的是主机的 IP 地址,并且这些 IP 地址是以网络字节序表示的。
插入小知识:网络字节序
计算机中存储数据有大端序和小端序两种方式。为保证网络中数据的一致性,出现了网络字节序,其实就是大端序。
比如,一个整数 0x12345678,在大端序中存储为 12 34 56 78,而在小端序中存储为 78 56 34 12 。
在网络编程中,规定网络传输的数据采用大端序,因此处理数据时要注意转换字节序。
例如,我们可以使用 htons 函数将主机字节序的短整数转换为网络字节序。
再来看我们的主角serv_addr:
结构体类型是sockaddr_in,这里先不详说,等到下个部分会展开说的。
我们就先知道,与服务端连接的时候,要设置以下三个:
serv_addr.sin_family = AF_INET;//这个AF_INET就是代表ipv4的意思,这个地方基本是固定不动的。serv_addr.sin_port = htons(atoi(argv[2]));//argv[2]是我们输入的时候的端口号,先用atoi函数(作用是将一个字符串转换成int),然后再用htons函数(作用是将主机字节序转化成网络字节序的short类型)。memcpy(&serv_addr.sin_addr, k_host->h_addr_list[0], k_host->h_length);//这个是设置我们所要连接的服务端的ip地址
第三部分:
if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("connect");close(sock_fd);return -1;}
第三部分就是和服务端取得连接,这里我们抽象的理解,利用connect函数连接了之后,我们以后就可以用这个套接字描述符来抽象的面对服务端,也就是说我们可以把这个描述符就当做服务端。
这里有个地方,是关于(struct sockaddr*)&serv_addr
。
为什么这个地方要从sockaddr_in类型强制转换成 sockaddr类型呢?
这是因为在网络编程的底层接口设计中,sockaddr
是一个更通用的结构体类型。虽然我们实际使用的是 sockaddr_in
来专门处理 IPv4 的情况,但在一些系统函数(比如 connect
)的参数要求中,需要的是 sockaddr
类型。
这样做可以保持接口的通用性和兼容性,使得函数能够处理不同类型的地址(比如 IPv6 或者其他网络协议的地址)。虽然我们这里明确是 IPv4 ,但为了满足函数的要求,还是需要进行这样的强制类型转换。另外,这种强制类型转换是被支持的,不会出错。
那为什么不一开始使用sockaddr呢?
这是因为 sockaddr
结构体比较通用但不够具体呀。它的成员定义比较简单,不能很好地满足我们在处理 IPv4 时的具体需求。
而 sockaddr_in
结构体专门为 IPv4 进行了设计,比如有专门的 sin_port
来表示端口,sin_addr
来表示 IP 地址。
虽然最终在某些函数调用时需要强制转换为 sockaddr
,但在前期的设置和操作中,使用 sockaddr_in
能让我们的代码更易读、更易编写和理解。
第四部分:
char buf[1024];for (int i = 0; i < 3; i++) {int i_res;memset(buf, 0, sizeof buf);sprintf(buf, "%d", i);if ((i_res = send(sock_fd, buf, strlen(buf), 0)) <= 0) {perror("send");break;}cout << "send: " << buf << endl;memset(buf, 0, sizeof buf);if ((i_res = recv(sock_fd, buf, sizeof buf, 0)) <= 0) {perror("recv");break;}cout << "recv: " << buf << endl;sleep(1);}
这一部分就是数据的发送和接收。
先看send函数的部分,这里就是我们发送数据的内容。
其实我们会发现,这和我们在操作文件的时候有很多相似的地方,其实他们本质都差不多,都是利用描述符来进行输入输出等操作。
这里没有太多好讲的,参数部分看了函数也就大概知道什么意思了,就不多讲了。
这里稍微说一下:
send
函数返回实际发送的字节数,recv
函数返回实际接收的字节数。
// send返回值说明:
// >0: 成功发送的字节数
// 0: 可能发生在非阻塞socket且发送缓冲区满时
// -1: 错误(需检查errno)// recv返回值说明:
// >0: 接收到的字节数
// 0: 连接已正常关闭(FIN接收)
// -1: 错误(需检查errno)
send函数的第四个参数是flags,用于指定发送数据时的一些特殊行为。我们一般默认填0,代表没有什么特殊操作。了解就好。
第五部分:
其实很简单,就是关闭我们的描述符。
close(sock_fd);
总结:
我们稍微的总结一下大概的流程:
首先我们用socket函数来创建一个套接字描述符int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
,然后接下里的任务就是用connect函数将fd和serv_addr绑定在一起。
也就是说我们要先设置好serv_addr(sockaddr_in类型)
,然后用connect函数。
设置serv_addr,需要准备好ip地址,端口还有协议。
ip地址我们要用到hontent类型,将其用gethostbyname函数赋值。
端口就用htons(atoi(val))。
协议我们已知ipv4,也就是AF_INET。
然后用connect函数绑定好了,我们就可以使用send和recv函数。
最后不要忘记close就好了。
服务端:
首先先贴上代码:
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
using std::cout;
using std::endl;int main(int argc, char* argv[]) {if (argc != 2) {cout << "USING: ./demo2 5005" << endl;return -1;}int listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd == -1) {perror("socket");return -1;}struct sockaddr_in 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(atoi(argv[1]));if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("bind");close(listen_fd);return -1;}if (listen(listen_fd, 5) != 0) {perror("listen");close(listen_fd);return -1;}int client_fd = accept(listen_fd, 0, 0);if (client_fd == -1) {perror("accept");close(listen_fd);return -1;}char buf[1024];while (true) {int i_res;memset(buf, 0, sizeof(buf));i_res = recv(client_fd, buf, sizeof(buf), 0);if (i_res == 0) {cout << "END" << endl;break;}if (i_res <= 0) {perror("recv");break;}cout << "recv: " << buf << endl;memset(buf, 0, sizeof(buf));strcpy(buf, "ok");if ((i_res = send(client_fd, buf, strlen(buf), 0)) <= 0) { perror("send");break;}cout << "send: " << buf << endl;}close(client_fd);close(listen_fd);return 0;
}
我们大体发现,其实和客户端的内容有很多地方是相通的,所以我们这里着重介绍一下不同的地方就好了。
两个套接字描述符:listenfd和clientfd
先说这两个套接字描述符的作用:
在服务器启动时,需要将服务器的地址(IP 地址和端口号)绑定到一个套接字描述符上,这个描述符就是 listenfd。这个套接字是用于监听客户端的连接的请求的。当有客户端需要连接的时候,我们会用clientfd来建立一个描述符,这个描述符会用来帮助进行客户端和服务端的数据交流。
用这两个套接字描述符来分开处理连接和数据交流的功能,这样,服务器可以同时处理多个客户端的连接请求,提高并发处理能力。当有新的客户端连接请求到来时,服务器可以继续使用 listenfd 监听其他连接请求,而使用新的 clientfd 与新的客户端进行通信。还有一个原因是代码阅读起来比较方便。
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
这里有一个小细节s_addr,这个其实是一个宏,对应的是h_addr_list[0]。
还有的是INADDR_ANY,这个是指示服务器监听所有可用的网络接口上的连接。
用htonl函数,因为地址的数字,用long而不是short。所以不用htons
bind函数
这里的bind函数不是std::bind函数,而是socket里面用于将监听描述符绑定服务器的地址和端口号的。与客户端里的connect函数类似。
listen(listenfd,nums)
listen函数是用来限制监听描述符的消息队列的长度,以及把listenfd从主动状态变成被动状态(官方话术)。
其实大概的意思我们可以拿客户端和服务端举例,例如客户端一般是主动的那一方,会发送消息,请求什么之类的,而服务端是等待客户端,然后根据其做出回应,像是被动状态。而listen函数就是把描述符变成被动状态,可以用来监听。
accept函数
accept(listenfd, (struct sockaddr *)&client_addr, &client_addr_len);
第一个参数是监听描述符,第二个是client_addr的强制转换,第三个是client_addr_len,这个变量我们最开始会初始化是sizeof(sock_addr_in)。
但是accept函数调用之后会实际存储的客户端地址信息的长度修改在client_addr_len。
当然在一些情况,后面两个参数我们可以填0。
填0代表我们并不关心客户端的ip地址和信息的长度大小。
accept函数的返回值是clientfd客户端套接字描述符。
other:
send函数和recv函数就和客户端的差不多,只不过要注意我们要发送和接受的描述符都是客户端套接字描述符。
最后要记得close,而且两个套接字描述符我们都要关闭。
总结:
所以我们首先先用socket函数创建一个listenfd,然后我们的目的是用bind函数将listenfd和服务端的地址等信息绑定起来。
所以我们要用sock_addr_in来设置,设置好了之后用bind函数绑定。
然后再用listen函数将listenfd变成被动状态,再用accpet函数创建出来clientfd。
之后就可以正常的发送接收消息,最后close即可。
封装
看上去很复杂,但是我们将他们封装之后,就会很简洁:
//客户端的main函数代码
int main(int argc, char* argv[]) {if (argc != 3) {cout << "USING: ./demo1 192.168.11.132 5005" << endl;return -1;}TCPclient client;client.connect(argv[1], atoi(argv[2]));std::string buf;for (int i = 0; i < 6; i++) {int i_res;buf = std::to_string(i);client.send(buf);cout << "send: " << buf << endl;client.recv(buf, 1024);cout << "recv: " << buf << endl;sleep(1);}return 0;
}//服务端的main函数代码
int main(int argc, char* argv[]) {if (argc != 2) {cout << "USING: ./demo2 5005" << endl;return -1;}TCPserver server;server.Init(atoi(argv[1]), 5);server.Accept();std::string buf;while (true) {int i_res;i_res = server.Recv(buf, 1024);if (i_res == false) break;cout << "recv: " << buf << endl;i_res = server.Send("ok");cout << "send: ok" << endl;}return 0;
}
服务端:
class TCPserver {
private:struct sockaddr_in serv_addr;public:int _listen_fd;int _client_fd;std::string _client_ip;unsigned short _port;TCPserver() :_listen_fd(-1), _client_fd(-1) {}bool Init(const unsigned short port, int max_size) {if (_listen_fd != -1) return false;_listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_fd == -1) return false;_port = port;struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);if (bind(_listen_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {close(_listen_fd);_listen_fd = -1;return false;}if (listen(_listen_fd, max_size) == -1) {close(_listen_fd);_listen_fd = -1;return false;}return true;}bool Accept() {struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);_client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);if (_client_fd == -1) return false;_client_ip = inet_ntoa(client_addr.sin_addr);return true;}bool Send(const std::string& buf) {if (_client_fd == -1) return false;int i_res;if ((i_res = send(_client_fd, buf.c_str(), buf.size(), 0)) <= 0) return false;return true;}bool Recv(std::string& buf, const size_t max_len) {if (_client_fd == -1) return false;buf.clear();buf.resize(max_len);int read_len;if ((read_len = recv(_client_fd, &buf[0], buf.size(), 0)) <= 0) {buf.clear();return false;}buf.resize(read_len);return true;}bool CloseListen() {if (_listen_fd == -1) return false;close(_listen_fd);_listen_fd = -1;return true;}bool CloseClient() {if (_client_fd == -1) return false;close(_client_fd);_client_fd = -1;return true;}~TCPserver() {CloseListen();CloseClient();}
};
客户端:
class TCPclient {
public:int _client_fd;std::string _ip;unsigned short _port;TCPclient() :_client_fd(-1) {}bool connect(const std::string& ip, const unsigned short port) {if (_client_fd != -1) return false;int sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd == -1) {perror("connect");return false;}_client_fd = sock_fd, _ip = ip, _port = port;struct hostent* h = gethostbyname(ip.c_str());if (h == 0) {perror("connect: gethostbyname failed");::close(_client_fd);_client_fd = -1;return false;}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);memcpy(&serv_addr.sin_addr, h->h_addr, h->h_length);if (::connect(_client_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("connect");::close(_client_fd);_client_fd = -1;return false;}return true;}bool send(const std::string& buf) {if (_client_fd == -1) return false;int i_res;if ((i_res = ::send(_client_fd, buf.c_str(), buf.size(), 0)) <= 0) return false;return true;}bool recv(std::string& buf, const size_t max_len) {if (_client_fd == -1) return false;buf.clear();buf.resize(max_len);int read_len;if ((read_len = ::recv(_client_fd, &buf[0], buf.size(), 0)) <= 0) {buf.clear();return false;}buf.resize(read_len);return true;}bool close() {if (_client_fd == -1) return false;::close(_client_fd);_client_fd = -1;return true;}~TCPclient() { close(); }};