Socket echo server
目录
一 前置知识
1. IP地址/端口号/网络字节序
2. Socket常见API
3. sockaddr结构
二 UDP echosever
API接口
UDP_Client.cc
UDP_Server.cc
UDP_Server.hpp
三 TCPechosever
API接口
一 前置知识
1. IP地址/端口号/网络字节序
在网络中,表示一个主机的唯一性就是IP地址,要跨网络传输,必须先要知道目的主机的IP地址才能发送过去。
当目的主机收到数据,交给对应的进程,在系统中标识进程的唯一性是PID,但在网络中标识进程的唯一性是端口号,那么为什么直接复用系统的PID呢?
最主要的原因:如果网络也按照PID标识进程的唯一性,那么系统就和网络关联起来了,有一方如果要改PID会影响到另一方,所以为了避免这种互相牵连的问题,才有了端口号。
1. 端口号可以指定
2. 一个进程可以绑定多个端口号,一个端口号只能被一个进程使用
端口号取值范围:0 ~ 65535,其中 0 ~ 1023 标识知名端口号,比如 110,120,119,说110就知道报警,说报警就知道110。知名端口号比如:HTTP(80),HTTPS(443),DNS(53)。
不同的主机存储数据方式可能是大端也可能是小端,如果不按统一的方式读取,可能读到的数据是乱序的,所以为了解决这类问题,在网络传输中默认使用大端。
2. Socket常见API
// 创建 socket 文件描述符int socket(int domain, int type, int protocol);domain:表示进行什么通信
- AF_UNIX:本地通信
- AF_INET:网络通信
type:表示以什么类型进行通信
- SOCK_STREAM:流式传输
- SOCK_DGRAM:数据包传输
protocol:表示套接字采用什么协议,一般domain和type就能表示具体的协议了,所以一般设置为0// 绑定端口号 (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):客户端向服务器发起连接
3. sockaddr结构
struct sockaddr {sa_family_t sa_family; // 地址族(如 AF_INET、AF_UNIX)char sa_data[14]; // 协议特定的地址信息(如 IP + 端口)
};
一个通用的套接字地址结构,用来接收不同的通信方式,并强转提取第一个字段 sa_family_t 判断。
struct sockaddr_in IPv4 套接字地址结构,表示网络传输
struct sockaddr_in {sa_family_t sin_family; // 地址族(必须为 AF_INET)in_port_t sin_port; // 16 位端口号(网络字节序)struct in_addr sin_addr; // 32 位 IPv4 地址(网络字节序)char sin_zero[8]; // 填充字段(未使用)
};struct in_addr {uint32_t s_addr; // IPv4 地址(32 位,网络字节序)
};
struct sockaddr_un Unix 域套接字地址结构,表示本地通信
struct sockaddr_un {sa_family_t sun_family; // 地址族(必须为 AF_UNIX/AF_LOCAL)char sun_path[108]; // 文件系统路径(如 "/tmp/my_socket")
};
因为他们的第一个字段是一样的,所以可以用 sockaddr_in和sockaddr_un强转成sockaddr结构,并提取第一个字段判断是AF_INET还是AF_UNIX选择强转成sockaddr_in还是sockaddr_un结构,也就是C语言版的继承和多态。
二 UDP echosever
API接口
- 创建套接字 > socket(int AF_INET, int SOCK_DGRAM ,int 0);
- 创建 struct sockaddr结构,并填充字段,通过bind和socket的返回描述符关联起来
int bind( int sockfd, // socket返回的描述符const struct sockaddr *addr, // 主机信息socklen_t addrlen // 他的长度);
- sendto:发送消息
sendto( int sockfd, // 套接字的返回值const void *buf, // 发送缓冲区size_t len, // 缓冲区大小int flags, // 额外选项const struct sockaddr *dest_addr, // 发给谁,通用sockaddr 结构,目的地址socklen_t addrlen // sockaddr的地址);
- recvfrom:接收消息
ssize_t recvfrom(int sockfd, // 套接字描述符void *buf, // 接收缓冲区size_t len, // 缓冲区大小int flags, // 读方式struct sockaddr *src_addr, // 谁发的socklen_t *addrlen // 他的长度);
端口号转大端API:htons();
IP地址转大端API:inet_addr();
代码示例:
UDP_Client.cc
// UDP_Client.cc#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <cstring>
int main(int argc,char* argv[])
{if(argc<3){std::cout<<"argc < 3 "<<std::endl;exit(-1);}// 创建套接字int fd=socket(AF_INET,SOCK_DGRAM,0);// 创建网络结构struct sockaddr_in sk;memset(&sk,0,sizeof(sk));// 填充信息并转大端sk.sin_family=AF_INET;sk.sin_port=htons(std::stoi(argv[2]));sk.sin_addr.s_addr=inet_addr(argv[1]);socklen_t sklen=sizeof(sk);std::cout<<argv[1]<<" "<<argv[2]<<std::endl;// 当首次调用sendto ,客户端会自动进行bind IP地址和端口号,所以不需要显示的bind// 后续的读写操作和 server.hpp 一样while(1){std::string ss = "client sendto: ";std::string mes;std::getline(std::cin,mes);ss+=mes;int n=sendto(fd,ss.c_str(),ss.size(),0,(struct sockaddr*)&sk,sklen);if(n<0){std::cout<<"client sengto failed "<<std::endl;exit(-1);}struct sockaddr des;socklen_t deslen;char buff[1024];n=recvfrom(fd,buff,sizeof(buff),0,&des,&deslen);if (n < 0){std::cout<<"client sengrecvfromto failed "<<std::endl;exit(-1);}else if(n==0)break;else{buff[n]=0;std::cout<<buff<<std::endl;}}close(fd);return 0;
}
UDP_Server.cc
#include "UDP_Server.hpp"int main(int argc,char* argv[])
{if(argc<2){std::cout<<"argc < 3 "<<std::endl;exit(-1);}// 提取IP地址std::string s=argv[1];// 传入端口号和IP地址构造UDP_server user(/*字符串转整数*/atoi(argv[2]),s);// 初始化创建套接字,binduser.Init();// 读写数据user.start();return 0;
}
UDP_Server.hpp
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <cstring>
class UDP_server
{
public:// 初始化端口号和IP地址UDP_server(uint16_t port, const std::string &ip) : _port(port), _ip(ip){}~UDP_server(){// 关闭套接字文件if (_socked != -1)close(_socked);}void Init(){// 创建套接字文件_socked = socket(AF_INET, SOCK_DGRAM, 0);if (_socked < 0){std::cout << "socket failed" << std::endl;exit(-1);}// 创建网络结构struct sockaddr_in local;memset(&local, 0, sizeof(local));// 填充网络类型/端口号/IP地址,转大端local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());// 和socket文件关联起来,并让sockaddr知道是sockadr_in结构int n = bind(_socked, (struct sockaddr *)&local, sizeof(local));if (n < 0){std::cout << "bind failed" << std::endl;exit(-1);}}void start(){std::cout<<_ip<<" "<<_port<<std::endl;_Running = true;while (_Running){char buff[1024];struct sockaddr_in sk;socklen_t sklen = sizeof(sk);int n = recvfrom(_socked, buff, sizeof(buff)-1, 0, (struct sockaddr *)&sk, &sklen);// 读到了数据if (n > 0){buff[n]=0;std::cout<<buff<<std::endl;// 回显回去sendto(_socked,buff,sizeof(buff),0,(struct sockaddr*)&sk,sklen);}else if (n < 0){std::cout << "recvfrom failed" << std::endl;exit(-1);}else if (n == 0){std::cout << "client out" << std::endl;break;}}}private:// 套接字描述符int _socked;// 端口号uint16_t _port;// IP地址std::string _ip;bool _Running;
};
三 TCPechosever
API接口
int listen( int sockfd, // 套接字返回值 int backlog // 全连接最大数量 ); // 监听事件
int accept(int sockfd, // 套接字返回的描述符 struct sockaddr *addr, // 发送方的信息socklen_t *addrlen // 长度); // 等待连接
int connect(int sockfd, // 套接字返回的描述符const struct sockaddr *addr, // 客户端信息socklen_t addrlen // 长度); //向目的主机发起连接
ssize_t recv(int sockfd, // accept返回值 void *buf, // 接收缓冲区size_t len, // 接收缓冲区大小int flags // 怎么读); // 接收数据
ssize_t send(int sockfd, // accept的返回值 const void *buf, // 发送缓冲区size_t len, // 发送缓冲区大小int flags // 怎么发); // 发送数据
TCP和UDP通信大致逻辑相同,主要TCP要和目的主机建立连接来管理这条连接,也就是connect接口,而服务器bind之后要等待连接的到来,所以要监听listen,当连接到来要获取accpet。
1. 当server端初始化的时候,bind之后就需要listen进行监听事件,等待客户端建立连接。
2. 当server启动的时候,就需要accpet获取连接,也就是有连接来了,获取到的连接分配一个文件描述符来进行读写,所以要起多线程/多进程,不然会阻塞后续的accpet。
3. 客户端创建好套接字,然后向服务器请求连接connect。
3. 当客户端退出调用close(),server端也要close()accpet返回的文件描述符来关闭连接,避免文件描述符泄露。