TCP/IP网络编程基础
网络协议:TCP/IP
img
应用层:主要负责为用户提供网络服务。应用层协议包括HTTP、FTP、SMTP等。 传输层:主要负责在网络中建立端到端的连接,提供可靠的数据传输。传输层协议包括TCP和UDP。 网络层:主要负责网络地址的分配和路由选择,例如 IP 协议。 数据链路层:主要负责传输数据帧,例如以太网、ATM 和 PPP 等协议。
参考资料:
https://zhuanlan.zhihu.com/p/620485741
进程间通信:IPC
管道:pipe 信号:signal 信号量:semaphore 消息队列:message 共享内存:share memory 套接字:socket
参考资料:
https://zhuanlan.zhihu.com/p/502627174
套接字:socket
img
服务端和客户端初始化Socket,得到文件描述符 服务端调用bind,绑定IP和端口 服务端调用listen,进行监听 服务端调用accept,等待客户端连接 客户端调用connect,向服务端发起连接请求。(TCP三次握手) 服务端调用accept返回用于传输的Socket的文件描述符(和第一点得到的Socket不同) 客户端使用write写入数据,服务端调用read读取数据 客户端断开连接时会调用close,服务端也会调用close(TCP四次挥手)
参考资料:
https://blog.csdn.net/OYMNCHR/article/details/124728256
最简单的服务端
Cygwin 安装教程:
https://blog.csdn.net/weixin_44778232/article/details/127579150
server.cpp
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
using std::string;
int main()
{
// 1. 创建 socket
int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockfd < 0)
{
printf("create socket error: errno=%d errmsg=%s\n", errno, strerror(errno));
return 1;
}
else
{
printf("create socket success!\n");
}
// 2. 绑定 socket
string ip = "127.0.0.1";
int port = 8080;
struct sockaddr_in sockaddr;
std::memset(&sockaddr, 0, sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = inet_addr(ip.c_str());
sockaddr.sin_port = htons(port);
if (::bind(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
{
printf("socket bind error: errno=%d, errmsg=%s\n", errno, strerror(errno));
return 1;
}
else
{
printf("socket bind success: ip=%s port=%d\n", ip.c_str(), port);
}
// 3. 监听 socket
if (::listen(sockfd, 1024) < 0)
{
printf("socket listen error: errno=%d errmsg=%s\n", errno, strerror(errno));
return 1;
}
else
{
printf("socket listen ...\n");
}
while (true)
{
// 4. 接收客户端连接
int connfd = ::accept(sockfd, nullptr, nullptr);
if (connfd < 0)
{
printf("socket accept error: errno=%d errmsg=%s\n", errno, strerror(errno));
return 1;
}
char buf[1024] = {0};
// 5. 接收客户端的数据
size_t len = ::recv(connfd, buf, sizeof(buf), 0);
printf("recv: conn=%d msg=%s\n", connfd, buf);
// 6. 向客服端发送数据
::send(connfd, buf, len, 0);
}
// 7. 关闭 socket
::close(sockfd);
return 0;
}
最简单的客户端
client.cpp
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
using std::string;
int main()
{
// 1. 创建 socket
int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockfd < 0)
{
printf("create socket error: errno=%d errmsg=%s\n", errno, strerror(errno));
return 1;
}
else
{
printf("create socket success!\n");
}
// 2. 连接服务端
string ip = "127.0.0.1";
int port = 8080;
struct sockaddr_in sockaddr;
std::memset(&sockaddr, 0, sizeof(sockaddr));
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = inet_addr(ip.c_str());
sockaddr.sin_port = htons(port);
if (::connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
{
printf("socket connect error: errno=%d errmsg=%s\n", errno, strerror(errno));
return 1;
}
// 3. 向服务端发送数据
string data = "hello world";
::send(sockfd, data.c_str(), data.size(), 0);
// 4. 接收服务端的数据
char buf[1024] = {0};
::recv(sockfd, buf, sizeof(buf), 0);
printf("recv: %s\n", buf);
// 5. 关闭 socket
::close(sockfd);
return 0;
}
Socket 封装
服务端:server2.cpp
#include <iostream>
#include <socket/socket.h>
using namespace yazi::socket;
int main()
{
Singleton<Logger>::instance()->open("./../server.log");
// 1. 创建 socket
Socket server;
// 2. 绑定 socket
server.bind("127.0.0.1", 8080);
// 3. 监听 socket
server.listen(1024);
while (true)
{
// 4. 接收客户端连接
int connfd = server.accept();
if (connfd < 0)
{
return 1;
}
Socket client(connfd);
char buf[1024] = {0};
// 5. 接收客户端的数据
size_t len = client.recv(buf, sizeof(buf));
printf("recv: connfd=%d msg=%s\n", connfd, buf);
// 6. 向客户端发送数据
client.send(buf, len);
}
// 7. 关闭 socket
server.close();
return 0;
}
客户端:client2.cpp
#include <iostream>
#include <socket/socket.h>
using namespace yazi::socket;
int main()
{
// 1. 创建 socket
Socket client;
// 2. 连接服务端
client.connect("127.0.0.1", 8080);
// 3. 向服务端发送数据
string data = "hello world";
client.send(data.c_str(), data.size());
// 4. 接收服务端的数据
char buf[1024] = {0};
client.recv(buf, sizeof(buf));
printf("recv: %s\n", buf);
// 5. 关闭 socket
client.close();
return 0;
}
阻塞 IO
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除阻塞状态。
非阻塞 IO
当用户线程发起read操作后,并不需要等待,而是马上得到结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发起read操作。如果内核中的数据准备好了,它就将数据拷贝到用户线程。
在非阻塞IO模型中,用户线程需要不断地轮询内核数据是否就绪,也就是说非阻塞IO不会交出CPU,而会一直占用CPU。
发送缓冲区
暂时无法在飞书文档外展示此内容
socket 没法直接将数据发送到网卡,所以只能先将数据发送到操作系统数据发送缓冲区。然后网卡从数据发送缓冲区中获取数据,再发送到接收方。
如果用户程序发送数据的速度比网卡读取的速度快,那么发送缓冲区将会很快被写满,这个时候 send 会被阻塞,也就是写入发生阻塞。
接收缓冲区
暂时无法在飞书文档外展示此内容
首先接收方机器网卡接收到发送方的数据后,先将数据保存到操作系统接收缓冲区。用户程序感知到操作系统缓冲区的数据后,主动调用接收数据的方法来获取数据。
如果数据接收缓冲区为空,这个时候 recv 会被阻塞,也就是读取发生阻塞。
注意:发送缓冲区和接收缓冲区这两个区域是每一个socket连接都有的。本质上而言,就是内核中的两块内存空间,socket创建完成后,这两块内存空间就开辟出来了。
SO_LINGER
设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。
参考资料:
https://blog.csdn.net/u012635648/article/details/80279338
SO_KEEPALIVE
不论是服务端还是客户端,一方开启 KeepAlive 功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。
参考资料:
https://www.cnblogs.com/1119reya/p/10382276.html
SO_REUSEADDR
SO_REUSEADDR是一个很有用的选项,一般服务器的监听socket都应该打开它。它的大意是允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接
参考资料:
https://zhuanlan.zhihu.com/p/79999012
服务端 socket
class ServerSocket : public Socket
{
public:
ServerSocket() = delete;
ServerSocket(const string &ip, int port);
~ServerSocket() = default;
};
客户端 socket
class ClientSocket : public Socket
{
public:
ClientSocket() = delete;
ClientSocket(const string & ip, int port);
~ClientSocket() = default;
};
IO 多路复用
在多路复用IO模型中,会有一个专门的线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。IO多路复用的优势在于,可以处理大量并发的IO,而不用消耗太多CPU/内存。
三种常用的轮询方法:select、poll、epoll
发表评论