Linux下socketAPI的使用,实现udp和tcp通信
阿里云国际版折扣https://www.yundadi.com |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
一.常用的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:作用同上用于保存套接字的长度
56两个参数都是用来保存客户端的socket信息的
输入提供一段空间
输出的时候用来表明是谁给服务器发送的消息
返回值返回读取到多少个字节读取失败就返回-1
注意src_addr是struct sockaddr类型的我们需要使用的是struct_sockaddr_in类型的所以我们在将其作为参数传入的时候需要强转成struct sockaddr类型的。
4.sendto()
参数
- sockfd:传入套接字的文件描述符
- buf:要发送的数据放在这个缓冲区中
- len:要发送数据的长度如果设为缓冲区的长度就是发送缓冲区内所有的信息
- flags:发送的方式默认为0就可以
- dest_addr:输入输出型参数用于保存目的进程的套接字信息
- addrlen:作用同上用于保存套接字的长度
56两个参数都是用来保存目的进程的socket信息的
输入提供一段空间
输出的时候用来表明是往哪个进程发信息目的进程的套接字信息
返回值返回发送了多少个字节读取失败就返回-1
注意这里的recvfrom和sendto实际上对应的是read和write本质上都是往文件中写数据。
5.listen函数-TCP
- 监听函数将当前服务进程设置为可接收连接的状态。
- 参数sockfd:监听套接字的fdbacklog全连接队列的长度-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和portOS会自动帮我们绑定
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发送消息给serverserver接收到消息并处理然后返回给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字节IPinet_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获取点分十进制的字符串ipinet_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 portport是主机序列是小端要转成网络序列
//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