目录
本文核心
预备知识
1.端口号
认识TCP协议
认识UDP协议
网络字节序
socket编程接口
sockaddr结构
UDP套接字编程
服务端
客户端
TCP与UDP传输的区别
可靠性:
传输方式:
用途:
头部开销:
速度:
linux相关的指令操作
ip地址转化函数
本文核心
预备知识
1.端口号
pid是进程管理的模块,纳入到网络板块不符合低耦合
万一进程版块出错,那么就会出现牵一发而动全身的效果
端口号:c:客户端 s:服务器
基于ip+端口的通信方式称为socket
进程如何绑定端口号呢?
通过哈希运算,把pcb绑定到特定的哈希表中
认识TCP协议
认识UDP协议
网络字节序
socket编程接口
基于ip+端口当通信方式称为socket(套接字)
套接字(Socket)是计算机网络通信中一个抽象层,它提供了进程间通信的端点。在网络编程中,套接字可以被看作是不同计算机进程间通信的一个虚拟端点,允许数据通过计算机网络进行传输。
以下是套接字的定义:
套接字:在计算机网络中,套接字是一个软件抽象层,它代表了一个网络连接的一端。每个套接字都有唯一的标识,由一个IP地址和一个端口号组成。套接字使得应用程序可以发送或接收数据,而不需要了解底层网络协议的细节。
套接字分为以下几种类型:
流套接字(Stream Sockets):提供面向连接、可靠的数据传输服务,通常使用TCP(传输控制协议)来实现。适用于需要数据完整性和顺序保证的应用,如Web浏览器和电子邮件服务器。
数据报套接字(Datagram Sockets):提供无连接的数据传输服务,通常使用UDP(用户数据报协议)来实现。适用于不需要可靠传输的应用,如视频会议或在线游戏,它们可以容忍一定的数据丢失。
原始套接字(Raw Sockets):允许直接发送和接收IP协议数据包,通常用于特殊用途,如网络诊断工具或实现新的协议。
套接字的类型:
流套接字(SOCK_STREAM):提供可靠的、面向连接的服务,通常基于TCP协议。
数据报套接字(SOCK_DGRAM):提供不可靠的、无连接的服务,通常基于UDP协议。
原始套接字(SOCK_RAW):允许直接访问网络层协议,如IP或ICMP。
套接字的地址家族:
AF_INET:用于IPv4网络协议。
AF_INET6:用于IPv6网络协议。
AF_UNIX:用于Unix域套接字,用于同一主机上的进程间通信。
求同存异
可以看到,调用接口的时候必须强转成const struct sockaddr*的结构,但是不同协议簇使用的套接字是不同的,那怎么做到统一呢?
他们前16位是一样的大小,可以确定协议簇的类型,只需要强转之后取前16字节,就可以获得不同的结构。
socket 常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockaddr结构
UDP套接字编程
服务端
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
using namespace std;
using func_t = std::function<std::string(const std::string&)>; //using xxx = 类型,是C++风格的typede
//typedef std::function<std::string(const std::string&)> func_t;
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
uint16_t defaultport = 8080; //端口号是一个16位无符号整数
uint32_t defaultip = INADDR_ANY; //ip地址是一个32位无符号整数 #define INADDR_ANY ((in_addr_t) 0x00000000)
const int sz = 1024;
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport, string ip = to_string(defaultip)) //运行的时候,只需要知道端口号和ip即可
: _sockfd(0)
, _ip(ip)
, _port(port)
, _isrunning(false)
{
bzero(&_local, sizeof(_local));
_local.sin_family = AF_INET; //网络层协议族为IPv4
_local.sin_addr.s_addr = inet_addr(_ip.c_str()); //将ip字符串转换为网络字节序的32位整数
_local.sin_port = htons(_port); //将端口号转换为网络字节序的16位整数
/*
local.sin_family = AF_INET;
local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
// local.sin_addr.s_addr = htonl(INADDR_ANY);
/*
单网络接口:如果你的服务器只有一个网络接口,设置 INADDR_ANY 意味着无论客户端通过哪个IP地址连接到这个服务器,服务端都会接受连接。
多网络接口:如果你的服务器有多个网络接口,每个接口有不同的IP地址,设置 INADDR_ANY 则意味着无论客户端连接到哪个IP地址,服务端都会接受连接。
例如,服务器可能有一个IP地址用于内部网络,另一个IP地址用于外部网络。
多IP地址:如果服务器的一个网络接口配置了多个IP地址(比如通过虚拟接口或别名),设置 INADDR_ANY 允许服务端在这些所有IP地址上接受连接。
也就是说,如果这个服务器存在多个ip,那么客户端与服务器通信时,可以接入本服务器的任意一个ip
*/
//将peer暂时初始化,后续会获得peer的地址信息
bzero(&_peer, sizeof(_peer));
_peer.sin_family = AF_INET;
_peer.sin_addr.s_addr = INADDR_ANY;
_peer.sin_port = htons(0);
}
~UdpServer()
{
if (_sockfd) close(_sockfd);
}
public:
void Init()
{
//1.创建UDP套接字
_sockfd = socket(AF_INET,SOCK_DGRAM, 0); //int socket(int domain, int type, int protocol);
if (_sockfd < 0)
{
lg(Fatal, "socket create error, sockfd: %d", _sockfd);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", _sockfd);
//绑定端口号和ip地址
if (bind(_sockfd, (const struct sockaddr*)&_local, sizeof(_local)) < 0)
{
lg(Fatal, "bind error, sockfd: %d", _sockfd);
exit(BIND_ERR);
}
lg(Info, "bind success, sockfd: %d, ip: %s, port: %d", _sockfd, _ip.c_str(), _port);
}
//对代码进行分层
void Run(func_t func) //这个地方需要传入一个函数指针
{
_isrunning = true;
char buffer[sz] = {0};
while (_isrunning)
{
socklen_t len = sizeof(_peer);
cout << "recv not over" << endl;
ssize_t n = recvfrom(_sockfd, buffer, sz, 0, (struct sockaddr*)&_peer, &len);
if (n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue; //出错了,继续接收下一个数据包
}
cout << "recv over " <<endl;
buffer[n] = 0;
string data = buffer;
string echo = func(data);
sendto(_sockfd, echo.c_str(), echo.size(), 0, (const struct sockaddr*)&_peer, len);
}
}
private:
int _sockfd; //套接字描述符,一切皆文件
string _ip; //我们使用的ip一般是字符串样式
uint16_t _port;
bool _isrunning;
struct sockaddr_in _local;
struct sockaddr_in _peer;
};
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
string ExcuteCommand(const string& cmd)
{
FILE *fp = popen(cmd.c_str(), "r");
if(nullptr == fp)
{
perror("popen");
return "error";
}
std::string result;
char buffer[4096];
while(true)
{
char *ok = fgets(buffer, sizeof(buffer), fp);
if(ok == nullptr) break;
result += buffer;
}
pclose(fp);
return result;
}
std::string Handler(const std::string &str)
{
std::string res = "Server get a message: ";
res += str;
std::cout << res << std::endl;
return res;
}
// ./udpserver port
int main(int argc, const char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = stoi(argv[1]);
unique_ptr<UdpServer> svr = make_unique<UdpServer>(port);
svr->Init();
svr->Run(Handler);
return 0;
}
需要注意的是
1.在对sockaddr_in成员进行初始化时,需要满足网络通讯的要求:1.端口号为网络字节序 2.ip地址从字符串风格编程点分式
2.recvfrom和sendto面向的是udp通信方式,传参的时候需要将sockaddr_in结构进行强转。
3.核心为:1.创建套接字 2.绑定端口号 3.接受发送
创建套接字:
绑定端口ip时(sockaddr_in)初始化时,端口号一般ip采用通用ip。
单网络接口:如果你的服务器只有一个网络接口,设置 INADDR_ANY 意味着无论客户端通过哪个IP地址连接到这个服务器,服务端都会接受连接。
多网络接口:如果你的服务器有多个网络接口,每个接口有不同的IP地址,设置 INADDR_ANY 则意味着无论客户端连接到哪个IP地址,服务端都会接受连接。
例如,服务器可能有一个IP地址用于内部网络,另一个IP地址用于外部网络。
多IP地址:如果服务器的一个网络接口配置了多个IP地址(比如通过虚拟接口或别名),设置 INADDR_ANY 允许服务端在这些所有IP地址上接受连接。
也就是说,如果这个服务器存在多个ip,那么客户端与服务器通信时,可以接入本服务器的任意一个ip。
在计算机网络中,存在一些端口号范围被操作系统保留用于特定的服务或进程。通常,这些端口号被称为“知名端口”(Well-known ports),它们的范围是从0到1023。
端口是一个16位的数字,可供选择。
4.用socket接口返回的套接字本质是一个文件描述符,不用的时候应该关掉sockfd。
5.popen接口
参数1:传入需要执行的命令
作用:
在内部自动fork,让父子进程建立管道,让子进程执行命令,将子进程的执行结果通过管道返回给调用方。
调用方想得到command的执行结果,可以用FILE*文件指针的方式读取
参数二:执行结果的打开方式(把这个命令当成一个文件)
客户端
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./udpclient serverip serverport
int main(int argc, const char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string ip = argv[1];
uint16_t port = (uint16_t)stoi(argv[2]);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(ip.c_str());
serveraddr.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cout << "create socket error" << endl;
return -1;
}
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 系统什么时候给我bind呢?首次发送数据的时候
string msg;
char buffer[1024];
while (1)
{
cout << "Please Enter@ ";
getline(cin ,msg);
sendto(sockfd, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
cout << "sendto over" << endl;
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len); //udp的数据报(数据传输模式)类型的数据需要用recvfrom读取,tcp的字节流可以用read读取
if(n > 0)
{
buffer[n] = 0;
cout << buffer << endl;
}
}
close(sockfd);
return 0;
}
客户端不需要手动绑定端口号,只需要OS去绑定即可。
客户端需要对服务端的sockaddr_in结构合理的初始化,才能接收和发送到正确的Server。
TCP与UDP传输的区别
字节流(Byte Stream)和数据报(Datagram)是网络通信中两种不同的传输方式,它们在数据传输的可靠性、传输方式和用途上有所区别:
可靠性:
字节流:通常指的是面向连接的传输方式,如TCP(传输控制协议)。它提供了可靠的数据传输,确保数据按照发送顺序到达,且不会丢失或重复。
数据报:通常指的是无连接的传输方式,如UDP(用户数据报协议)。它不保证数据的可靠传输,数据包可能会丢失、重复或到达顺序错乱。
传输方式:
字节流:在字节流传输中,数据像水流一样连续传输。发送方和接收方之间存在一个持续的连接,数据按照顺序到达。
数据报:数据报传输将数据分割成小的、独立的数据包进行发送。每个数据包携带目的地址信息,但不保证按照顺序到达,也不保证所有数据包都能到达。
用途:
字节流:适用于需要高可靠性的应用,如网页浏览、文件传输、电子邮件等,这些应用需要确保数据的完整性和顺序。
数据报:适用于对实时性要求高,但可以容忍一定丢包率的场景,如视频会议、在线游戏等。这些应用更关注数据的快速传输而不是完整性。
头部开销:
字节流:由于需要维护连接状态,通常头部开销较大,因为TCP头部包含了序列号、确认号、窗口大小等信息。
数据报:UDP头部相对简单,开销较小,只包含少量的控制信息。
速度:
字节流:由于其可靠性机制,如重传机制,可能会影响传输速度。
数据报:因为没有复杂的可靠性保证机制,通常传输速度较快。
在选择字节流还是数据报时,需要根据应用的具体需求来决定。如果数据完整性和顺序至关重要,应该选择字节流;如果实时性更重要,能够容忍一定的数据丢失,那么数据报可能是更好的选择。
telnet不能链接udp服务,因为telnet底层采用的是tcp的协议。
linux相关的指令操作
netstat,可以查看本地的服务器
telnet 127.0.0.1
本地环回连接服务器,常用于测试服务器是否存在BUG。