cpp网络编程-socket搭建http服务器

本文介绍了如何使用C++进行TCP网络编程,包括服务器端和客户端的socket创建、连接、数据收发及关闭过程。通过示例代码展示了服务器监听、接受连接以及客户端发起连接的基本步骤,并讲解了TCP的三次握手和四次挥手过程。
摘要由CSDN通过智能技术生成

cpp网络编程

socket c/s 搭建

服务器端

sockfd = socket();    // 创建一个socket,赋给sockfd
bind(sockfd, ip::port和一些配置);    // 让socket绑定端口,同时配置连接类型之类的
listen(sockfd);        // 让socket监听之前绑定的端口
while(true)
{
    connfd = accept(sockfd);    // 等待客户端连接,直到连接成功,之后将客户端的套接字返回出来
    recv(connfd, buff); // 接收到从客户端发来的数据,并放入buff中
    send(connfd, buff); // 将buff的数据发回客户端
    close(connfd);      // 与客户端断开连接
}

用户端

sockfd = socket();    // 创建一个socket,赋给sockfd
connect(sockfd, ip::port和一些配置);    // 使用socket向指定的ip和port发起连接
scanf("%s", buff);    // 读取用户输入
send(sockfd, buff);    // 发送数据到服务端
recv(sockfd, buff);    // 从服务端接收数据
close(sockfd);        // 与服务器断开连接

代码实现

// server.cpp
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<sys/socket.h>
#include<sys/unistd.h>
#include<sys/types.h>
#include<sys/errno.h>
#include<netinet/in.h>
#include<signal.h>

#define BUFFSIZE 2048
#define DEFAULT_PORT 16555
#define MAXLINK  2048

int sockfd,connfd;

void stopServerRunning(int p){
    close(sockfd);
    printf("clode server\n");
    exit(0);
}
int main(){
    struct sockaddr_in servaddr;
    char buff[BUFFSIZE];

    sockfd = socket(AF_INET, SOCK_STREAM,0);
    if(-1 == sockfd){
        printf("Create socket error(%d):%s\n",errno,strerror(errno));
        return -1;
    }
    bzero(&servaddr , sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(DEFAULT_PORT);
    if(-1 == bind(sockfd,(struct sockaddr*)&servaddr,sizeof servaddr)){
        printf("Bind error(%d):%s\n",errno,strerror(errno));
        return -1;
    }
    if (-1 == listen(sockfd, MAXLINK))
    {
        printf("Listen error(%d): %s\n", errno, strerror(errno));
        return -1;
    }
    printf("Listening...\n");

    while(true){
        signal(SIGINT,stopServerRunning);

        connfd = accept(sockfd,NULL,NULL);
        if (-1 == connfd)
        {
            printf("Accept error(%d): %s\n", errno, strerror(errno));
            return -1;
        }
        bzero(buff,BUFFSIZE);
        recv(connfd,buff,BUFFSIZE-1 ,0);
        printf("Recv: %s\n",buff);

        send(connfd,buff,strlen(buff),0);
        close(connfd);
    }
    return 0;

}

// client.cpp
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <sys/types.h>
#include <sys/errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFFSIZE 2048
#define SERVER_IP "172.16.1.150"    // 指定服务端的IP,记得修改为你的服务端所在的ip
#define SERVER_PORT 16555            // 指定服务端的port

int main()
{
    struct sockaddr_in servaddr;
    char buff[BUFFSIZE];
    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == sockfd)
    {
        printf("Create socket error(%d): %s\n", errno, strerror(errno));
        return -1;
    }
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
    servaddr.sin_port = htons(SERVER_PORT);
    if (-1 == connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
    {
        printf("Connect error(%d): %s\n", errno, strerror(errno));
        return -1;
    }
    printf("Please input: ");
    scanf("%s", buff);
    send(sockfd, buff, strlen(buff), 0);
    bzero(buff, sizeof(buff));
    recv(sockfd, buff, BUFFSIZE - 1, 0);
    printf("Recv: %s\n", buff);
    close(sockfd);
    return 0;
}

服务器操作

# 编译
g++ server.cpp -o server.o
g++ client.cpp -o client.o

# 运行
./server
# 另一个窗口
./client

运行结果

image-20230724104917662

socket库函数

使用socket(也就是套接字)编程的时候,其实际上便是工作于应用层和传输层之间,此时我们可以屏蔽掉底层细节,将网络传输简化为:

A的应用层→A的传输层→B的传输层→B的应用层

如果使用的是TCP连接的socket连接的话,每个数据包的发送的过程大致为:

  • 数据通过socket套接字构造符合TCP协议的数据包
  • 在屏蔽底层协议的情况下,可以理解为TCP层直接将该数据包发往目标机器的TCP层
  • 目标机器解包得到数据

步骤为:建立连接–>收发数据–>断开连接

  1. 建立连接

    TCP三次握手(c–connect(SYN)–>s | s(listen) --SYN|ACK–> c | c --ACK(SYN)–> s)

  2. 断开连接

    TCP四次挥手(A(close)–FIN–>B | B–ACK–>A | B–FIN–>A | A–ACK–>B)

socket函数

#include <sys/socket.h>
int socket(int family, int type, int protocol);

套接字描述符本质上也是提供给程序可以对其缓存区进行读写,程序在其写缓存区写入数据,写缓存区的数据通过网络通信发送至另一端的相同套接字的读缓存区,另一端的程序使用相同的套接字在其读缓存区上读取数据,这样便完成了一次网络数据传输

  • family:AF_INET IPv4协议族 | AF_INET6 IPv6协议族
  • type:SOCK_STREAM:字节流套接字(TCP|SCTP) |SOCK_DGRAM:数据报套接字(UDP)|SOCK_SEQPACKET:有序分组套接字(SCTP)|SOCK_RAW:原始套接字(直接与网络层通信)
  • protocol:指定协议,0为默认,TCP(IPPROTO_TCP)
  • 返回值:SUCCES(套接字描述符),ERROR(-1)-errnostrerror(errno)

bind函数

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

#include <netinet/in.h>
struct in_addr
{
    in_addr_t       s_addr;         // 32位IPv4地址
};
struct sockaddr_in
{
    uint8_t         sin_len;        // 结构长度,非必需
    sa_family_t     sin_family;     // 地址族,一般为AF_****格式,常用的是AF_INET
    in_port_t       sin_port;       // 16位TCP或UDP端口号
    struct in_addr  sin_addr;       // 32位IPv4地址
    char            sin_zero[8];    // 保留数据段,一般置零
};

struct sockaddr_in servaddr;    // 定义一个IPv4套接字地址结构体
bzero(&servaddr, sizeof(servaddr));    // 将该结构体的所有数据置零
servaddr.sin_family = AF_INET;    // 指定其协议族为IPv4协议族
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    // 指定IP地址为通配地址
servaddr.sin_port = htons(DEFAULT_PORT);    // 指定端口号为16555

将套接字与ip::port绑定 | 把一个本地协议地址赋予一个套接字

  • 套接字地址结构体:sockaddr --> (IPv4)sockaddr_in=(family,port,addr)[赋值使用字节排序函数:htons() | htonl()]

    #include <netinet/in.h>
    uint16_t htons(uint16_t host16bitvalue);    //host to network, 16bit
    uint32_t htonl(uint32_t host32bitvalue);    //host to network, 32bit
    uint16_t ntohs(uint16_t net16bitvalue);     //network to host, 16bit
    uint32_t ntohl(uint32_t net32bitvalue);     //network to host, 32bit
    

listen函数

close --> listen

  • backlog:服务器套接字处于LISTEN状态下所维护的**已完成连接队列(Accept队列)**的长度和的最大值
#include <sys/socket.h>
int listen(int sockfd, int backlog);

connect函数

用于客户端跟绑定了指定的ip和port并且处于LISTEN状态的服务端进行连接,客户端发起TCP三次握手

服务端调用bind函数的时候无需指定ip,但客户端调用connect函数的时候则需要指定服务端的ip。

ip地址的表达格式与数值格式相互转换函数:

  • inet_pton()函数用于将IP地址从表达格式转换为数值格式
  • inet_ntop()函数用于将IP地址从数值格式转换为表达格式
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

accept函数

用于从Accept队列中pop出一个已完成的连接。若Accept队列为空,则accept函数所在的进程阻塞。

  • 返回值:对应客户端套接字描述符(accept)
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

recv函数 & send函数

recv函数用于通过套接字接收数据,send函数用于通过套接字发送数据

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
  • sockfd:要读写的套接字
  • *buff:指定要接收数据的空间的指针(recv)或要发送的数据(send)
  • nbytes:指定最大读取的字节数(recv)或发送的数据的大小(send)
  • flags:用于设置一些参数,默认为0
  • 返回值:读取到的或写入的字节数

close函数

关闭套接字,终止TCP连接

#include <unistd.h>
int close(int sockfd);

搭建HTTP服务器

可以使用像http://192.168.19.12:16555/这样的URL进行服务器请求,并且能得到一个合法的返回

修改:返回值修改为HTTP返回串

http请求串

方法名 URL 协议版本  //请求行
字段名:字段值       //消息报头
字段名:字段值       //消息报头
...
字段名:字段值       //消息报头
请求正文           //可选

每一行都以\r\n结尾,表示一个换行

http返回串

协议版本 状态码 状态描述 //状态行
字段名:字段值       //消息报头
字段名:字段值       //消息报头
...
字段名:字段值       //消息报头
响应正文           //可选

状态码:

  • 1xx:指示信息,表示请求已接收,继续处理
  • 2xx:成功,表示请求已被成功接收、理解、接受
  • 3xx:重定向,要完成请求必须进行更进一步的操作
  • 4xx:客户端错误,请求有语法错误或请求无法实现
  • 5xx:服务器端错误,服务器未能实现合法的请求

编写返回串

void setResponse(char *buff) {  
 bzero(buff, sizeof(buff)); 
 strcat(buff, "HTTP/1.1 200 OK\r\n"); 
 strcat(buff, "Connection: close\r\n"); 
 strcat(buff, "\r\n"); 
 strcat(buff, "Hello\n"); 
}

// main
setResponse(buff);

发送结果

curl -v "http://172.16.1.150:16555/"

image-20230724160914529

io优化

将server服务器中的printf输出改为文件输出。

#define LOG_BUFFSIZE 65536
class Logger{
    char buff[LOG_BUFFSIZE];
    int buffLen;
    FILE *fp;
public:
    Logger(){
        bzero(buff, sizeof(buff));
        buffLen = 0;
        fp = fopen("Server.log", "a");
    }
    void Flush(){
        fputs(buff, fp);
        bzero(buff, sizeof(buff));
        buffLen = 0;
    }
    void Log(const char *str, int len){
        if (buffLen + len > LOG_BUFFSIZE - 10){
            Flush();
        }
        for (int i = 0; i < len; i++){
            buff[buffLen] = str[i];
            buffLen++;
        }
    }
    ~Logger() {
        if (buffLen != 0)   {
            Flush();
        }fclose(fp);
    }
}logger;

// 替换掉printf("Recv: %s\n", buff);
logger.Log("Recv: ", 6);
logger.Log(buff, strlen(buff));

//通过cat Server.log 查看输出日志

压力测试

ab -c 100 -n 10000 -r "http://172.16.1.150:16555/" # -r在出错的时候也继续压测
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dreaife-BW

感谢支持~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值