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
运行结果
socket库函数
使用socket(也就是套接字)编程的时候,其实际上便是工作于应用层和传输层之间,此时我们可以屏蔽掉底层细节,将网络传输简化为:
A的应用层→A的传输层→B的传输层→B的应用层
如果使用的是TCP连接的socket连接的话,每个数据包的发送的过程大致为:
- 数据通过socket套接字构造符合TCP协议的数据包
- 在屏蔽底层协议的情况下,可以理解为TCP层直接将该数据包发往目标机器的TCP层
- 目标机器解包得到数据
步骤为:建立连接–>收发数据–>断开连接
-
建立连接
TCP三次握手(c–connect(SYN)–>s | s(listen) --SYN|ACK–> c | c --ACK(SYN)–> s)
-
断开连接
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)-
errno
–strerror(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/"
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在出错的时候也继续压测