目录
一.常用的socketAPI
1.socket()-创建套接字
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数:
- domain:域,指套接字的种类,即你希望操作系统给你提供哪种服务,有很多种,一般我们用的最多的是AF_INET
- type:套接字的类型,也有很多种,用的最多的是SOCK_SRREAM和SOCK_DGRAM
- protocol:直接设为0,如果前两个参数确定,第三个参数也就确定了。
返回值:一个文件描述符,执行socket()函数创建套接字的时候,相当于把网络以文件的形式打开了。创建失败就返回小于0的数
2.bind()-绑定端口号
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
参数:
- socket:就是socket函数返回的文件描述符
- address:一个sockaddr类型的结构体指针,我们需要在其中指明一些信息(用哪种类型的网络服务,IP地址,端口号之类的信息),这里我们指明三个参数:sin_family sin_port sin_addrs.addr
- address_len:前面address结构体的字节数
返回值:如果绑定成功返回0,绑定失败返回-1
注意:bind(sock, (struct sockaddr*)&local, sizeof(local))传入第二个参数的时候需要进行强转
sockaddr结构
网络通信的标准方式有很多种,比如基于ip的网络通信,AF_INET,原始套接字,域间套接字。有不同种类的套接字,但是网络标准把它们统一化了,所以就有了sockaddr这一结构。
我们传入不同套接字的socketaddr结构的时候,把它们都强转成统一的sockaddr类型,然后根据前面的16位地址类型,判断是哪一种套接字。(就像是c++里面的多态,sockaddr就像是用C语言实现的父类)
IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。这里我一般使用的sin_family16位地址类型使用是AF_INET。
3.recvfrom()-接收数据的函数
服务器提供的服务一般都是死循环,就是一直提供服务。
参数:
- sockfd:传入套接字的文件描述符
- buf:读入的数据放在这个缓冲区中
- len:缓冲区的长度
- flags:读的方式,默认为0就可以
- src_addr:输入输出型参数,用于保存发送方的套接字信息
- addrlen:作用同上,用于保存套接字的长度
5,6两个参数都是用来保存客户端的socket信息的
输入:提供一段空间
输出的时候用来表明是谁给服务器发送的消息
返回值:返回读取到多少个字节,读取失败就返回-1
注意:src_addr是struct sockaddr类型的,我们需要使用的是struct_sockaddr_in类型的,所以我们在将其作为参数传入的时候,需要强转成struct sockaddr类型的。
4.sendto()
参数:
- sockfd:传入套接字的文件描述符
- buf:要发送的数据放在这个缓冲区中
- len:要发送数据的长度(如果设为缓冲区的长度就是发送缓冲区内所有的信息)
- flags:发送的方式,默认为0就可以
- dest_addr:输入输出型参数,用于保存目的进程的套接字信息
- addrlen:作用同上,用于保存套接字的长度
5,6两个参数都是用来保存目的进程的socket信息的
输入:提供一段空间
输出的时候用来表明是往哪个进程发信息(目的进程的套接字信息)
返回值:返回发送了多少个字节,读取失败就返回-1
注意:这里的recvfrom和sendto实际上对应的是read和write,本质上都是往文件中写数据。
5.listen函数-TCP
- 监听函数,将当前服务进程设置为可接收连接的状态。
- 参数:sockfd:监听套接字的fd;backlog:全连接队列的长度-1
- 返回值:成功返回0,失败返回-1
6.accept函数-TCP
用于接收连接用
参数:
- sockfd:监听套接字(就像鱼庄的拉客少年张三),这个套接字的作用是监听
- addr:输入输出型参数,用于保存发送方的套接字信息
- addrlen:输入输出型参数,用于保存发送方的套接字信息
返回值:如果接收成功,返回非0的整数,这个整数是一个文件描述符,也算是一个套接字,这个套接字的作用是提供确切的服务
7.connect函数-TCP
参数:同上面的accept
返回值:成功连接返回0,失败返回-1
二.实现简易的UDP通信
1.目标:
实现一个简单的UDP通信服务,client给server发送消息,server能够接收到消息并处理,返还结果给client,我这里实现的是client发送字符串指令,执行server服务器上的命令行操作,然后server将结果返回给client
2.大致思路:
server:
1.首先创建套接字,打开网络文件-socket接口
2.然后给该服务器绑定端口和ip(特殊处理)--bind接口
3.提供服务,接收客户端传过来的数据--recvfrom函数
4.响应客户端,将数据发送回客户端--sendto函数
注意点:
1.在给当前服务进程绑定端口号的时候,需要调用htons函数,因为这里的端口号是主机上的序列,需要转换成网络字节序
2.在给当前服务进程绑定IP地址的时候,不能指定绑死一个IP,因为当前服务端可能配置有多个IP地址,我们需要的不是从某个IP地址传过来的数据,我们需要的是从所有IP地址传过来,来访问当前port对应服务进程的数据,所以我们指定IP地址的时候,用local.sin_addr.s_addr = INADDR_ANY;表示可以接收从当前服务器上任一IP地址传过来的数据。
3.调用recvfrom函数接收数据的时候,要传入两个参数用来保存客户端方的socket信息,用于之后sendto函数,根据这里得到的socket信息,向客户端发送数据。
client:
1.创建套接字,打开网络文件--socket接口
2.从命令行参数中获取要访问的服务端的ip地址和port端口号,设置到自己定义的struct sockaddr_in的结构体中。注意int main(int argc, char *argv[]),命令行参数总共有三个,我们在命令行运行的时候要给出三个参数./udp_client server_ip server_port,如果给出的命令行参数不足3个,则打印出提示语句
3.调用sendto函数把数据通过套接字发送给服务端,注意将带有服务端socket信息的sockaddr_in结构体传入
4.调用recvfrom函数接收服务端返回的数据,可以定义一个sockaddr_in类型的tmp用于保存服务端的socket信息(实际上在命令行参数里面已经给出了,这里充当的是一个占位符,如果之后要用到,比如从别的主机接收数据,那时候再使用)
注意点:
1.客户端不需要显示的bind端口号。首先,客户端要与服务端通信,必须要有socket,也就是IP地址+port端口号,但是,客户端不需要显示的bind!一旦显示bind,就必须明确,client要和哪一个port关联。然而如果我们写死与哪个端口号关联,比如8080,这个端口号可能已经被其他应用程序占用,所以我们不需要显示地bind端口号,只要有空闲的端口号,用就行了(客户端能跑起来就行)。client的端口号一般由OS自动bind,在client发送数据的时候,OS会自动bind,采用的是随机端口的方式。
服务端需要绑定端口号,而且必须明确,不能改变。因为要一直提供确切的服务。在服务端理论上也会有端口号互相冲突的情况,但是服务端的接口和port之间一般会进行系统的管理,所以一般都不会冲突。
3.代码实现
server.cc
/*
* @Author: zebra
* @Date: 2023-02-03 23:11:43
* @LastEditTime: 2023-02-04 13:55:27
* @LastEditors: zebra
* @Description:
* @FilePath: /cpp/learning/f1_blog/a11_socketAPI/udp_server.cc
* by zebra
*/
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
const uint16_t port = 8080;
int main()
{
// 1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket create error: " << errno << std::endl;
return 1;
}
// 2. 绑定IP和端口号
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port); // 此处的端口号,是前面定义的一个变量,属于主机上的一个变量,是主机序列,需要转换成网络字节序
local.sin_addr.s_addr = INADDR_ANY; //一个服务器上可能有多张网卡,对应多个IP,因此我们不能绑死一个IP地址,而是将任一IP同一个port接收到的数据都拿上来。
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error : " << errno << std::endl;
return 2;
}
// 3. 提供服务
bool flag = false;
#define NUM 1024
char buffer[NUM];
while (!flag)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (cnt > 0)
//cnt表示收到了多少字节的数据
{
buffer[cnt] = 0; //0=='\0',我们收到的是字符串数据
FILE *fp = popen(buffer, "r"); //执行buffer中的指令
std::string ans;
char line[1024] = {0};
while(fgets(line, sizeof(line), fp) != NULL){
ans += line;
}
pclose(fp);
std::cout << "client message get! content: " << buffer << std::endl;
sendto(sock, ans.c_str(), ans.size(), 0, (struct sockaddr *)&peer, len);
}
}
return 0;
}
client:
/*
* @Author: zebra
* @Date: 2023-02-03 23:11:48
* @LastEditTime: 2023-02-04 13:54:54
* @LastEditors: zebra
* @Description:
* @FilePath: /cpp/learning/f1_blog/a11_socketAPI/udp_client.cc
*/
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 0;
}
// 1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error : " << errno << std::endl;
return 1;
}
//client不需要显示bind绑定ip和port,OS会自动帮我们绑定
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
// 2.使用服务,发起请求
while (1)
{
std::cout << "client: ";
char line[1024];
fgets(line, sizeof(line), stdin);
sendto(sock, line, strlen(line), 0, (struct sockaddr*)&server, sizeof(server));
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
if(cnt > 0)
{
buffer[cnt] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
4.运行结果
client:
server:
三.实现简易的TCP通信
1.目标
建立简易的TCP通信,client发送消息给server,server接收到消息并处理,然后返回给client。
2.大致思路
server
1.创建套接字,和UDP同理
2.bind服务的IP和port,和UDP同理
3.tcp是面向连接的,所以一定有人主动建立(客户端,需要服务,要服务所以主动请求建立连接),一定有人被动接受连接(服务器,提供服务)。因此在服务端,设置套接字为Listen状态,本质是让当前服务进程允许其他用户来连接
4.调用accept函数接收连接,这里返回的文件描述符是真正提供服务的套接字
5.如果接收连接成功,提供服务
由于tcp连接是流式套接,所以可以像对文件一样操作套接字(这里指的是提供服务的套接字,不是listen监听套接字)
6.当有连接建立成功的时候,创建一个线程来为client提供服务。
1).pthread_create创建一个线程来执行任务,进入我们定义的handlerRequest函数,注意要把new_sock的指针作为参数传过去(这里要创建一个新的int对象)
2).在处理函数内部首先调用pthread_detach方法分离线程,让线程结束后自动释放资源。这样就不需要主线程来join了(主线程来join那就变成串行了)
3).拿出传入的套接字,然后释放传入的参数资源(因为我们传入参数的时候是创建了一个int对象的,我们在函数里面拿到对应的值以后,要把int对象的空间释放,避免资源浪费)
4).提供服务,关闭new_sock套接字
client
1.可以在命令行指定要访问服务的ip和port,注意从命令行参数argv里面拿出来的port是字符串,我们需要的是uint16_t类型的,所以需要使用atoi函数或者stoi函数。
2.创建套接字,同理
3.client无需显示绑定端口号,让OS自动帮我们绑定即可(如果你显示绑定,其他client进程随机绑定,随机到你显示绑定的这个port,那你显示绑定的client进程就无法运行了)
4.往sockaddr_in里面写入要访问服务进程的ip,port,使用的服务等等信息。
1).注意我们的port是主机序列,是小端,要转成网络序列。ip地址是字符串(htoi)。
2).我们的ip是点分十进制的字符串风格的IP,要转化成为4字节IP(inet_addr)
5.通过connect函数连接服务器,对应的就是accept接收
6.进行业务请求
3.代码实现
server.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void ServiceIO(int new_sock)
{
while (true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer) - 1);
// 调用read函数对提供服务的new_socket像文件一样操作
if (s > 0)
{
buffer[s] = 0; // 将获取的内容当成字符串
std::cout << "client message get! content: " << buffer << std::endl;
std::string echo_string = "server get your message: ";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0)
{
// 如果read返回0,说明client端关闭了(socket连接关闭的时候会发送一个FIN消息,此时read返回0)
std::cout << "client quit ..." << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
/**
* @description: 该函数交给创建的线程来执行
* @param {void} *args
* @return {*}
*/
void *HandlerRequest(void *args)
{
pthread_detach(pthread_self());
int sock = *(int *)args; //拿出传入的套接字
delete (int*)args; //释放传入的参数资源(因为我们传入参数的时候是创建了一个int对象的,我们在函数里面拿到对应的值以后,要把int对象的空间释放,避免资源浪费)
ServiceIO(sock);
close(sock);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
// tcp server
// 1. 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
std::cerr << "socket error: " << errno << std::endl;
return 2;
}
// 2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error: " << errno << std::endl;
return 3;
}
// 3. 建立连接
const int back_log = 5;
if (listen(listen_sock, back_log) < 0)
{
std::cerr << "listen error" << std::endl;
return 4;
}
signal(SIGCHLD, SIG_IGN); // 在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if (new_sock < 0)
// 如果接收链接失败,就continue跳过服务代码,循环重新尝试接收连接
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port); // 从struct sockaddr_in里面获取port并转成主机序列;
std::string cli_ip = inet_ntoa(peer.sin_addr); // 从struct sockaddr_in获取点分十进制的字符串ip,inet_ntoa函数,传入结构体里面的一个结构体sin_addr;该函数的作用不仅要把4字节的ip地址转换成点分十进制的字符串形式,还要把网络序列转换成主机序列
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl; // new_sock对应的是文件接收连接成功后返回的文件描述符
pthread_t tid;
int *pram = new int(new_sock);
pthread_create(&tid, nullptr, HandlerRequest, pram);
}
return 0;
}
client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string svr_ip = argv[1];
uint16_t svr_port = (uint16_t)atoi(argv[2]);
//1. 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
struct sockaddr_in server;
bzero(&server, sizeof(server)); //将缓冲区的空间全部清零
server.sin_family = AF_INET;
//1. 将点分十进制的字符串风格的IP,转化成为4字节IP
//2. 将4字节由主机序列转化成为网络序列
server.sin_addr.s_addr = inet_addr(svr_ip.c_str()); //server ip,我们的ip是点分十进制的字符串风格的IP,要转化成为4字节IP
server.sin_port = htons(svr_port); // server port,port是主机序列,是小端,要转成网络序列
//2. 发起链接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cout << "connect server failed !" << std::endl;
return 3;
}
std::cout << "connect success!" << std::endl;
// 进行业务请求
while(true)
{
std::cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
write(sock, buffer, strlen(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s>0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
4.运行结果
client
server