文章目录
TCP网络编程
server(服务器代码的编写)
-
1.初始化
-
2.进入一个主循环(死循环)
a.读取客户端发来的"请求"(Request)
b.根据请求内容,计算生成"响应"(Respnose)内容最核心
c.把响应数据返回给客户端
socket():创建套接字
调用格式
- 1.使用socket()函数创建一个文件描述符
int socket(AF_INET,SOCK_DGRAM,0);
//参数一:表示改文件描述符使用的是IPV4
//参数二:表示该文件描述符使用的是UDP协议
//参数三:一般不使用
-
参数domain 用来说明网路程序所在的主机采用的是那种通信协议簇,这些协议族再头文件<sys/socket.h>中定义
-
AF_INET //表示IPv4网络协议
-
AF_INET6 //IPv6网络协议
-
参数type 用来指明创建的套接字类型,
-
SOCK_STREAM:流式套接字,面向连接可靠的通信类型
-
SOCK_DGRAM:数据报套接字,非面向连接和不可靠的通信类型
-
SOCK_RAW:原始套接字,用来直接访问IP协议
-
参数protocol 指定套接字使用的协议,一般采用默认值0,表示让系统根据地址格式和套接字类型,自动选择一个合适的协议
-
返回值:
-
调用成功就创建了一个新的套接字并返回它的描述符,在之后对该套接字的操作中都要借助这个文件描述符,
-
否则返回-1(<0) 表示套接字出错.应用程序可调用WSAGetLastError() 获取相应的错误代码
bind ():将套接字绑定到指定的网络地址
使用这个函数前需要将服务器的ip和端口号赋值到结构体sockaddr_in中
使用bind()函数前的准备工作
- sockaddr 结构:针对各种通信域的套接字,存储它们的地址信息
struct sockaddr{
u_short sa_family; /*16位协议家族*/
char sa_data[14] /*14字节协议地址*/
};
- sockaddr_in结构:专门针对Internet通信域,存储套接字相关的网络地址信息(IP地址、传输层端口号等信息)
struct sockadd_in{
short int sin_family; //地址家族
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //IP地址
unsigned char sin_zero[8]; //全为0
};
- in_addr 结构:专门用来存储IP地址
struct in_addr{
unsigned long s_addr;
};
#include<netinet/in.h>
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
-
参数sockfd:是未经绑定的套接字文件描述符,是由socket()函数返回的,要将它绑定到指定的网络地址上
-
参数addr : 是一个指向sockaddr结构变量的指针,所指结构体中保存着特定的网络地址,就是要把套接字sockfd绑定到这个地址上.
-
参数 addrlen :是结构sockaddr结构的长度,等于sizeof(struct sockadd
-
返回值
返回0:表示已经正确的实现了绑定
如果返回SOCKET_ERROR表示有错。应用程序可调WSAGetLastError()获取相应的错误代码
- 最后在函数调用的时候,将这个结构强制转换成sockaddr类型
本机字节序和网络字节序
-
本机字节序在不同的计算机中,存放多字节值的顺序是不一样的,有的是从低到高,有的是从高到低.计算机中的多字节数据的存储顺序称为本机字节顺序.
-
网络字节序:在网络协议中,对多字节数据的存储,有它自己的规定。多字节数据在网络协议报头中的存储顺序,称为网络字节序.在套接字中必须使用网络字节序
所以把IP地址和端口号装入套接字时,我们需要将本机字节序抓换成网络字节序,在本机输出时,我们需要将它们从网络字节序转换成本机字节序
-
套接字编程接口专门为解决这个问题设置了4个函数
-
1.htons():短整数本机顺序转换为网络顺序,用于端口号
-
2.htonl():长整数本机顺序转换成网络顺序,用于IP地址
-
3.ntohs():短整数网络顺序转换为本机顺序,用于端口号
-
4.ntohl():长整数网络顺序转换为本机顺序,用于IP地址
这4个函数将被转换的数值作为函数的参数,函数返回值作为转换后的结果
- 点分十进制的IP地址的转换
在Internet中,IP地址常常是用点分十进制的表示方法,但在套接字中,IP地址是无符号的长整型数,套接字编程接口设置了两个函数,专门用来两种形式IP地址的转换
- 1.inet_addr函数
unsigned long inet_addr(const char* cp);
/*
入口参cp:点分十进制形式的IP地址
返回值:网络字节顺序的IP地址,是无符号的长整数
*/
- 2.inet_ntoa函数
char* inet_ntoa(struct in_addr in)
/*
入口参数in: 包含长整数IP地址的in_addr结构变量
返回值:指向点分十进制IP地址的字符串的指针
*/
listen():启动服务器,监听客户机端的连接请求
调用格式
int listen(SOCKET s,int backlog);
-
参数s:服务器端的套接字描述符,一般先进行绑定到熟知的服务器端口,要通过他监听来自客户端的连接请求,一般将这个套接字称为监听套接字
-
参数backlog:指定监听套接字的等待连接缓冲区队列的最大长度,一般为5
-
返回值:正确执行则返回0出错返回SOCKET_ERROR
-
函数功能:本函数适用于支持连接的套接字,在Internet通信域,仅用于流式套接字,并仅用于服务器端
accept():接收连接请求
调用格式
SOCKET accept(SOCKET s,struct sockaddr* addr,int * addrlen);
-
参数s:服务器端监听套接口描述符,调用listen()后,该套接口一直在监听连接
-
参数addr:可选参数,指向sockaddr结构的指针,该结构用来接收下面通信层所知的请求连接以方的套接字的网络地址
-
一个出口参数,用来但会下面通信层所指的对方连接实体的网络地址。
-
addr参数的实际格式由套接字创建时所产生的地址家族确定。
-
-
参数addrlen:可选参数,指向整形数的指针.用来返回addr地址的长度
- addrlen参数也是一个出口参数,在调用时初始化为addr所指的地址长度,在调用结束时它包含了实际返回的地址的字节长度,如果addr与addrlen中有一个为nullptr,将不返回所接收的远程套几口的任何地址信息.
-
返回值: 如果正确执行,则返回一个SOCKET类型的文件描述符,否则,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetError()来获取特定的错误代码
-
函数功能: 本函数从监听套接字s的等待队列中抽取第一个连接请求,创建一个与s同类的新的套接口,来与请求连接的客户套接字创建连接通道,如果连接成功,就返回新创建的套接字描述符,并且监听套接字采用阻塞工作方式,则accept()阻塞调用它的进程,直到新的连接请求出现.
recv():从一个已经连接套接口接收数据
调用格式
int recv(SOCKET s,char* buf,int len,int flag);
-
参数s: 套接字描述符,表示一个接口端已经与对端建立连接的套接口
-
参数buf: 用于接收数据的字符缓冲区指针,这个缓冲区是用户进程的接收缓冲区
-
参数len: 用户缓冲区长度,以字节大小 计算
-
参数flag: 指定函数的调用方式,一般设置为0
-
返回值: 如果执行正确,返回从套接字s实际读入到buf中的字节数,如果连接已终止,返回0;否则的话,返回SOCKET_ERROR错误
-
recv()函数功能: s是接收端,既调用本函数一方所创建的本地套接字,可以是数据报套接字或者流式套接字,它已经与对方建立了TCP连接,该套接字的数据接收缓冲区中存有对方发送来的数据,调用recv()函数就是将本地的套接字数据接收缓冲区中的数据接收到用户进程的缓冲区中
send():向一个已连接的套接口发送数据
调用格式
int send(SOCKET s,char* buf,int len,int flags);
-
参数s : SOCKET 描述符,标识发送方已与对方建立连接的套接口,就是要借助连接从这个套接口 发送数据
-
参数buf:指向用户进程的字符缓冲区的指针,该缓冲区包含要发送的数据
-
参数len:用户缓冲区的数据的长度,以字节计算
-
参数flags:执行次调用的方式,此参数一般置为0
-
返回值: 如果执行正确,返回实际发送出去的数据的字节总数,要注意这个数字可能小于len中规定的大小;否则,返回SOCKET_ERROR
-
send()函数的调用功能:send()函数用于向本地已建立连接 的数据报或流式套接口发送数据,不论是客户机还是服务器应用程序都用send汉纳树向TCP连接的另一端发送数据.客户端程序一般用send()函数向服务器发送请求,服务器则用send()函数向客户机程序发送应答
实例代码
tcp_socket.hpp
#pragma once
#include<cstdio>
#include<cstring>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
class TcpSocket{
public:
TcpSocket()
:_fd(-1)
{
}
bool Socket(){
//和UDP 不同的是,第二个参数面向字节流(TCP)
_fd=socket(AF_INET,SOCK_STREAM,0);
if(_fd<0){
perror("socket");
return false;
}
return true;
}
//给服务器使用
bool Bind(const std::string&ip,uint16_t port){
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(ip.c_str());
addr.sin_port=htons(port);
int ret=bind(_fd,(sockaddr*) &addr,sizeof(addr));
if(ret<0){
perror("bind");
return false;
}
return true;
}
//给服务器使用
bool Listen(){
//listen 进入监听状态
//所谓的" 连接" 指的是一个五元组
//源ip,源端口,目的端口,协议
int ret=listen(_fd,10);
if(ret<0){
perror("listen");
return false;
}
return true;
}
//给服务器使用
bool Accept(TcpSocket* peer,std::string* ip=NULL,uint16_t* port =NULL){
//accept 从连接队列中取一个连接到用户代码中
//如果队列中没有连接,就会阻塞(默认行为)
sockaddr_in peer_addr;
socklen_t len=sizeof(peer_addr);
//返回值也是一个 socket
int client_sock=accept(_fd,(sockaddr*)&peer_addr,&len);
if(client_sock<0){
perror("accept");
return false;
}
peer->_fd=client_sock;
if(ip!=NULL){
*ip = inet_ntoa(peer_addr.sin_addr);
//把peer_addr所包含的IP地址转换成点分十进制
//交给用户
}
if(port!=NULL){
*port=ntohs(peer_addr.sin_port);
//把网络序转换成主机序
}
return true;
}
//客户端和服务器都会使用
bool Recv(std::string *msg) {
//括号后面的const 修饰this指针
msg->clear();
char buf[1024*10]={0};
ssize_t n=recv(_fd,buf,sizeof(buf)-1,0);
//recv的返回值,如果读取成功,返回结果为读到的字节数
//如果读取失败,返回结果为-1
//如果对端关闭了 socket 返回结果为 0
if(n<0){
perror("recv");
return -1;
}else if(n==0){
//需要考虑返回0的情况
return 0;
}
msg->assign(buf);
return true;
}
//客户端和服务器都会使用
bool Send(const std::string& msg) {
ssize_t n=send(_fd,msg.c_str(),msg.size(),0);
if(n<0){
perror("send");
return false;
}
return true;
}
//给客户端使用
bool Connect(const std::string &ip,uint16_t port){
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(ip.c_str());
addr.sin_port=htons(port);
int ret=connect(_fd,(sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("connect");
return false;
}
return true;
}
bool Close(){
if(_fd!=-1){
close(_fd);
}
return true;
}
private:
int _fd;
};
tcp_server.hpp
#pragma once
//通用的TCP 服务器
#include"tcp_socket.hpp"
#include<functional>
#define CHECK_RET(exp) if(!(exp)){\
return false;\
}
typedef std::function<void(const std::string&,std::string*)> Handler;
class TcpServer{
public:
TcpServer(){
}
bool Start(const std::string& ip,uint16_t port,
Handler handler){
//tcp 启动的基本流程
//1.先创建一个socket
CHECK_RET(listen_sock_.Socket());
//2.绑定端口号
CHECK_RET(listen_sock_.Bind(ip,port));
//3.进行监听
CHECK_RET(listen_sock_.Listen());
printf("Server start OK\n");
//4.进入主循环
while(true){
//5.通过accept 获取到一个连接
TcpSocket client_sock; //和客户端沟通的socket
std::string ip;
uint16_t port;
//核心问题在于,第一次Accept 之后就进入了一个循环
//在这个操作过程中,循环一直没有结束,Accept 没有被重复调用到
//后续链接过来的客户端都在内核中的链接队列中排队呢,一直得不到处理
//应该想办法让我们的程序能够更快速的调用到Accept
//多进程或者多线程解决
bool ret=listen_sock_.Accept(&client_sock,&ip,&port);
if(!ret){
continue;
}
printf("[%s:%d] 有客户端连接!\n",ip.c_str(),port );
//6.和客户端进行具体的沟通,一次连接
//就进行多次更新
while(true){
//1.读取请求
std::string req;
int r=client_sock.Recv(&req);
if(r<0){
continue;
}
if(r==0){
//对端关闭了 socket
client_sock.Close();
printf("[%s:%d] 对端关闭了连接\n",ip.c_str(),port);
break;
}
printf("[%s:%d] 对客户端发送了:%s\n",ip.c_str(),port,req.c_str());
//2.根据请求计算响应
std::string resp;
handler(req,&resp);
//3.把响应写回到客户端
client_sock.Send(resp);
}
}
}
private:
TcpSocket listen_sock_;
};
tcp_client.hpp
#pragma once
#include"tcp_socket.hpp"
//给用户提供的信息越少越好
class TcpClient{
public:
TcpClient(){
_sock.Socket();
}
~TcpClient(){
_sock.Close();
}
bool Connect(const std::string &ip,uint16_t port){
return _sock.Connect(ip,port);
}
int Recv(std::string *msg){
return _sock.Recv(msg);
}
bool Send(const std::string&msg){
return _sock.Send(msg);
}
private:
TcpSocket _sock;
};