【网络编程】套接字

本文详细介绍了如何使用C++实现UDP和TCP网络程序,包括封装UDP和TCP套接字,创建UDP服务器和客户端以实现英汉翻译功能,以及TCP服务器和客户端的基本通信流程。还探讨了地址转换函数如inet_pton、inet_aton等,并讨论了多进程和多线程在服务器端的应用。
摘要由CSDN通过智能技术生成

在这里插入图片描述

1.实现简单的UDP网络程序【实现英译汉功能】

1.1UDP传输接口

UDP套接字是无连接协议,必须使用sendto函数发生数据,必须使用recvfrom函数接收数据,发生时序指明目的地址。

1.1.1sendto函数原型
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
  • sendto(通过文件描述符发生数据给对方),用于UDP协议
参数:
    sockfd:文件描述符
    buf:发生数据的首地址
    len:发生数据的长度
    flags:
		0:默认方式发生数据
         MSG_OOB:发送带外数据
         MSG_DONTRIUTE:告诉IP协议,目的主机在本地网络,没必要查找路由器
         MSG_DONTWAIT:设置未非阻塞操作
         MSG_NOSIGNAL:表示发送动作不愿被SIGPIPE信号终止
         dest_addr:目标主机的IP地址和端口信息
         addrlen:sizeof(dest_addr)
        
返回值:
      成功:传输的字节数
      失败:-1,失败原因存于错误码
1.1.2 recvfrom函数

接收对方发送的数据

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
  • 通过sockfd文件描述符从对方接收数据,用于UDP协议
参数:
    sockfd:文件描述符
    buf:传入类型参数,接收数据的首地址
    len:可接收数据的最大长度
    flags:
		0:默认方式接收数据
         MSG_OOB:接收带外数据
         MSG_PEEK:查看数据标志,返回的数据并不在系统中删除,如果再次调用recv函数会返回相同的数据内容
         MSG_DONTWAIT:设置为非阻塞操作
         NSG_WAITALL:强迫接收len大小的数据才返回,除非有错误或者信号参数
         src_addr:传入类型参数,存放发送方的IP地址和端口信息
         addrlen:

返回值:
    成功:实际接收的字节数
   	失败:返回-1,失败原因存放在error中
1.2封装UdpSocket
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class UdpSocket {
public:
	UdpSocket() : fd_(-1) {
	}
    bool Socket() {
	fd_ = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd_ < 0) {
		perror("socket");
		return false;
	}
	return true;
	}
	bool Close() {
		close(fd_);
		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;
	}
    
    //这里的ip,port都是自定义的转出类型参数
    
    //当需要获取发送方的ip和port的时候,就可以传入参数获取发送方的ip和port信息。
	bool Recvfrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) {
		char tmp[1024 * 10] = {0};
		sockaddr_in peer;
		socklen_t len = sizeof(peer);
		ssize_t read_size = recvfrom(fd_, tmp,
		sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);
		if (read_size < 0) {
			perror("recvfrom");
			return false;
		}
		// 将读到的缓冲区内容放到输出参数中
		buf->assign(tmp, read_size);
		if (ip != NULL) {
			*ip = inet_ntoa(peer.sin_addr);
		}
		if (port != NULL) {
		*port = ntohs(peer.sin_port);
		}
		return true;
	}
	bool Sendto(const std::string& buf, 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);
		ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr,sizeof(addr));
		if (write_size < 0) {
			perror("sendto");
			return false;
		}
		return true;	
	}
private:
	int fd_;
};
1.3UDP通用服务器

服务器只能被动的响应,不能主动的发送。

#pragma once
#include "udp_socket.hpp"
// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lambda
#include <functional>
typedef std::function<void (const std::string&, std::string* resp)> Handler;
class UdpServer {
public:
	UdpServer() 
    {
		assert(sock_.Socket());
	}
	~UdpServer() {
		sock_.Close();
	}

//启动服务器函数
//handler是服务器具体的功能实现
bool Start(const std::string& ip, uint16_t port, Handler handler) {
	// 1. 创建 socket
	// 2. 绑定端口号
	bool ret = sock_.Bind(ip, port);
	if (!ret) {
		return false;	
	}
	// 3. 进入事件循环
	for (;;) {
	// 4. 尝试读取请求
		std::string req;
		std::string remote_ip;
		uint16_t remote_port = 0;
		bool ret = sock_.Recvfrom(&req, &remote_ip, &remote_port);
		if (!ret) {
			continue;
        }
        std::string resp;
		// 5. 根据请求计算响应
		handler(req, &resp);
		// 6. 返回响应给客户端
		sock_.Sendto(resp, remote_ip, remote_port);
		printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port,req.c_str(), resp.c_str());
		}	
		sock_.Close();
		return true;
	}
private:
		UdpSocket sock_;
};
1.4实现英汉翻译服务器
#include"Udp_server.hpp"
#include<unordered_map>
#include<iostream>
std::unordered_map<std::string,std::string>dict;
void translate(const std::string&req,std::string* resp)
{
	auto it= dict.find(req);
	if(it==dict.end())
	{
		return ;
 	}
	*resp=it->second;
}
int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        printf("Useage:./dict_server [ip] [port]...\n");
		return 1;
	}
	// 1. 数据库初始化
	dict.insert(std::make_pair("hello", "你好"));
	dict.insert(std::make_pair("world", "世界"));
	dict.insert(std::make_pair("c++", "最好的编程语言"));
	dict.insert(std::make_pair("cxk", "鸡哥"));
	dict.insert(std::make_pair("ikun","爱鲲"));
	dict.insert(std::make_pair("理智","荔枝"));
	UdpServer dict_server;
	dict_server.Start(argv[1],atoi(argv[2]),translate);
	return 0;
}
1.5UDP通用客户端

客户端要实现接收服务器的信息和发送信息给服务器的功能

#pragma once
#include "udp_socket.hpp"
class UdpClient {
public:
	UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
		assert(sock_.Socket());
	}
	~UdpClient() {
		sock_.Close();
	}
    //buf传入类型参数,获取服务器发送的信息
	bool RecvFrom(std::string* buf) {
        //不需要获取发送端的信息,所以不要传入ip和port参数
		return sock_.RecvFrom(buf);
	}
	bool SendTo(const std::string& buf) {
		return sock_.SendTo(buf,ip_, port_);
	}
private:
	UdpSocket sock_;
// 服务器端的 IP 和 端口号
	std::string ip_;
	uint16_t port_;
};
1.6英汉翻译客户端
#include"Udp_client.hpp"
#include<iostream>

int main(int argc,char* argv[])
{
	if(argc!=3)
	{
		printf("Useage:./dict_client [ip] [port]...\n");
		return 1;
 	}
	Udpclient dict_client(argv[1],atoi(argv[2]));
 	for(;;)
	{
		std::string word;
		std::cout<<"请输入要查询的单词:"<<std::endl;
		std::cin>>word;
		if(!std::cin)
		{
			std::cout<<"good bye"<<std::endl;
			break;
 		}
		dict_client.SendTo(word);
		std::string result;
		dict_client.RecvFrom(&result);
		std::cout<<word<<"的意思是:"<<result<<std::endl;
	}
	return 0;
}
1.7实现结果

在这里插入图片描述

2.地址转换函数

2.1字符串转in_addr地址函数:
2.1.1 inet_pton()函数
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
将点分十进制串转换为32位网络大端的数据
参数:
    af【版本】: AF_INET【IPV4】,AF_INET6【IPV6】
    src:点分十进制的首地址
    dst:传入类型参数,转换的结果
 
	char buf[]="192.168.2.125";
    unsigned int num=0;
    inet_pton(AF_INET,buf,&num);
    unsigned char* p=(unsigned char*)&num;
    printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));
输出:192 168 2 125
2.1.2 inet_aton()函数
int inet_aton(const char* strptr,struct in_addr*addrptr)
将strptr所指C字符串转换成一个32位的网络字节序二进制值,并同过addrptr指针来存储,成功返回1,失败返回0
 
	int ip_addr;
	struct in_addr inet_ip_addr;
	ip_addr = inet_aton("192.168.2.125", &inet_ip_addr);
	unsigned char* p=(unsigned char*)&inet_ip_addr.s_addr;
	printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));

输出:192 168 2 125
2.1.3 inet_addr()函数
in_addr_t inet_addr(const char *strptr);
作用:将一个点分10进制转换为一个ip地址【整形】
   
	int  ip_addr;
	ip_addr = inet_addr("192.168.2.125");   //设置ip点分十进制地址的地址
	if(ip_addr==INADDR_NONE)               //  返回值错误判断
  	 	printf("ERROR");
	unsigned char* p=(unsigned char*)&ip_addr;
	 printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));//打印转换后的网络字节序

输出: 192 168 2 125
2.2in_addr转字符串函数
2.2.1inet_ntoa()函数
char *inet_ntoa(struct in_addr in);
in:ip地址
       
     struct in_addr network;
	network.s_addr=2097326272;    //为s_addr赋值--网络字节序
	printf("IP : %s\n", inet_ntoa(network));  
输出:IP : 192.168.2.125
2.2.2 inet_ntop()函数【转换为点分10进制】
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
32位网络大端数据转换为点分十进制串
参数:
    af【版本】: AF_INET【IPV4】,AF_INET6【IPV6】
    src:32位网络大端数据
    dst:存储点分十进制串地址
    size:存储点分十进制串的长度,最多16位
 
返回值:
      存储点分十进制串数组首地址

int main()
{
	char ip[16];
	struct in_addr net_ip;
	net_ip.s_addr=2097326272;
	const char* ret;
	ret=inet_ntop(AF_INET,(void*)&net_ip.s_addr,ip,16);                                                      
    printf("ip:%s\n",ip);
	return 0;
}
输出:ip:192.168.2.125
2.3关于inet_ntoa
  • inet_ntoa函数返回了一个char*,则这个函数的内部申请了一块内存来保存字符串ip。
  • man手册上表明,inet_ntoa函数把这个结果放到了静态存储区,不需要手动的释放。
#include<stdio.h>
#include<netinet.h/in.h>
#include<arpa/inet.h>

int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr=0;
    addr2.sin_addr.s_addr=0xffffffff;
    char* ptr1=inet_ntoa(addr1.sin_addr);
    char* ptr2=inet_ntoa(addr2.sin_addr);
    printf("ptr1:%s\n",ptr1);
    printf("ptr2:%s\n",ptr2);
    return 0;
}
输出:
    ptr1:255.255.255.255
    ptr2:255.255.255.255
如果将inet_ntoa的调用顺序交换
输出:
     ptr1:0.0.0.0
	 ptr2:0.0.0.0

在这里插入图片描述

上面的结果说明后续调用的inet_ntoa会覆盖掉前一次上一次调用的结果。

  • 在多线程的条件下,由于进程地址空间共享,所以容易发生数据的覆盖。【最好的方法是加锁
  • 在APUE【UNIX环境高级编程】中, 明确提出inet_ntoa不是线程安全的函数;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问
    题;
  • 在Centos环境下,内部加了锁,所以不会出现异常。
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void* Func1(void* p) {
	struct sockaddr_in* addr = (struct sockaddr_in*)p;
	while (1) {
	sleep(1);
 	char* ptr = inet_ntoa(addr->sin_addr);
	printf("addr1: %s\n", ptr);
	}
	return NULL;
}
void* Func2(void* p) {
    struct sockaddr_in* addr = (struct sockaddr_in*)p;
    while (1) {
        sleep(1);
        char* ptr = inet_ntoa(addr->sin_addr);
	    printf("addr2: %s\n", ptr);
}
	return NULL;                                                                                                 
}
int main() {
    pthread_t tid1 = 0;
    struct sockaddr_in addr1;
	struct sockaddr_in addr2;
	addr1.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;
	pthread_create(&tid1, NULL, Func1, &addr1);
	pthread_t tid2 = 0;
	pthread_create(&tid2, NULL, Func2, &addr2);
 	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	return 0;
}

在这里插入图片描述

在centos环境下,内部实现了锁,所以不会出现异常。

3.TCP网络程序

3.1TCP socket----API
  • TCP传输特点:出错重传,每次发送数据对方都会会ACK【确认报文】,传输可靠,丢包率低。

socket()函数

创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
#include <sys/types.h>         
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数说明:
	domain:IP地址类型
			常用:AF_INET,AF_INET6
	type:套接字类型
			SOCK_STREAM:它提供基于字节流的有序、可靠、双向连接
可以支持带外数据传输机制。【流式套接字------用于TCP通信】
			SOCK_DGRAM:支持数据报(固定最大值的无连接、不可靠消息长度)。【报文套接字----UDP通信】
			SOCK_SEQPACKET
	protocol:默认0

返回值:
        成功:返回文件描述符
        失败:-1

domain参数

在这里插入图片描述

connect()函数

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数
    sockfd:sockfd套接字【文件描述符】
    addr:套接字结构体的地址
    addrlen:结构体的长度

bind()函数

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
参数:
    sockfd:套接字
    address:ip套接字结构体地址
    addrlen:结构体大小
         
返回值:
      成功返回0
      失败返回-1

listen()函数

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
参数:
    socket:套接字
    backlog:已完成连接队列和未完成连接队列数之和的最大值 128
  • 将套接字由自动变被动
  • 创建一个连接队列

在这里插入图片描述

accept()函数

// 接收请求 (TCP, 服务器)
t
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
参数:
    socket:监听套接字
    address:传入类型参数,获取客户端的IP和端口信息
    address_len:结构体大小的地址
返回值:
      新的已连接套接字的文件描述符
3.2TCP通信流程

TCP服务的通信流程

  • 创建监听套接字
  • 将监听套接字于IP和端口绑定(bind())
  • 监听listen(),等待请求。
  • 提取链接accept(),得到一个新的通信套接字
  • 通信
  • 关闭通信套接字

在这里插入图片描述

3.3封装TCP Socket
3.3.1 TCP传输接口:recv函数和send函数
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);

参数:
    sockfd:发送端的的套接字
    buff:存放接收数据的缓冲区
    nbytes:指明buff的长度
    flags:一般为0
  • ​ recv将数据接收到内核的接收缓冲区中。特点:先关心发送缓冲区是否有数据发送
  • ​ 如果调用recv如果发现:发送缓冲区正在发送数据,则等待发送缓冲区数据发送完毕。如果发送过程中网络断开,recv函数直接返回SOCKET_ERROR
  • ​ 如果调用recv,发送缓冲区数据已经发送完毕且网络正常。recv会先检查socket的接收缓冲区,如果接收缓冲区没有数据或者协议正在接收数据,recv就就一直等待,直到数据接收完毕。
  • ​ 接收缓冲区将数据接收完后,将数据copy至buff中。如果buff的长度小于接收缓冲区接收的数据长度,那么就需要多次recv。
  • ​ recv函数返回值:返回接收之后copy到buff的字节数。因此,一次recv返回不一定能够将接收缓冲区的数据全部copy出来。
ssize_t send(int s, const void *buff, size_t len, int flags);

参数:
     s:指明发送端套接字描述符
     buff:存放发送数据的缓冲区
     len:指明发送的数据字节数
     flags:0
  • ​ 如果send检查到要发送的字节数大于发送缓冲区的长度直接返回SOCKET_ERROR.
  • ​ 内核缓冲区剩余空间>发送字节数,如果此时内核缓冲区正在发送数据则等待发完,再继续发送;如果发送缓冲区没有正在发送数据,则直接发送。
  • ​ 内核缓冲区剩余空间<发送字节数,则send阻塞等待内核发送缓冲区的数据发送完,再发送当前字节。
  • send函数即使将数据成功送进发送缓冲区,单不代表能够成功将数据发送出去,如果此时数据还没发送,网络断掉,那么下一个send直接返回SOCKET_ERROR.与此同时,数据保留。
  • ​ send函数发现发送缓冲区的数据正在发送,那么它会在此等待数据传完,如果数据还没传完,网络中断,send立刻返回SOCKET_ERROR。为什么send会立刻返回SOCKET_ERROR,那是因为UNIX系统一旦发现网络断开,会向调用send的进程发送信号SIGPIPE,即终止进程。
  • ​ send函数返回值:如果send函数copy成功,返回copy进去的实际字节数。
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class TcpSocket {
public:
	TcpSocket() : fd_(-1) { }
	TcpSocket(int fd) : fd_(fd) { }
	bool Socket() 
    {
		fd_ = socket(AF_INET, SOCK_STREAM, 0);
		if (fd_ < 0) {
            perror("socket");
		   return false;
		}
		printf("open fd = %d\n", fd_);
		return true;
	}
	bool Close() const 
    {
		close(fd_);
		printf("close fd = %d\n", fd_);
		return true;
	}
	bool Bind(const std::string& ip, uint16_t port) const 
    {
		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(int num) const 
    {
        //fd_变为监听套接字
		int ret = listen(fd_,num);
		if (ret < 0) {
			perror("listen");
			return false;	
		}
		return true;
	}
	bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const 
    {
		sockaddr_in peer_addr;
		socklen_t len = sizeof(peer_addr);
		int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);
		if (new_sock < 0) {
			perror("accept");
			return false;
		}
		printf("accept fd = %d\n", new_sock);
		peer->fd_ = new_sock;
		if (ip != NULL) {
			*ip = inet_ntoa(peer_addr.sin_addr);
		}
		if (port != NULL) {
			*port = ntohs(peer_addr.sin_port);
		}
		return true;
	}
    bool Recv(std::string* buf) const 
    {
		buf->clear();
		char tmp[1024 * 10] = {0};
		// [注意!] 这里的读并不算很严谨, 因为一次 recv 并不能保证把所有的数据都全部读完
		// 参考 man 手册 MSG_WAITALL 节.
		ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
		if (read_size < 0) {
			perror("recv");
			return false;
		}
		if (read_size == 0) {
			return false;
		}
		buf->assign(tmp, read_size);
		return true;
	}
	bool Send(const std::string& buf) const 
    {
		ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
		if (write_size < 0) {
			perror("send");
			return false;
		}
		return true;
}
	bool Connect(const std::string& ip, uint16_t port) const 
    {
		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;
	}
	int GetFd() const {
		return fd_;
	}
private:
	int fd_;
}
3.4TCP通用服务端

服务端只能被动的发送信息。

#pragma once
#include <functional>
#include"TcpSocket.hpp"
typedef std::function<void (const std::string& req,std::string* resp)> Handler;
class tcp_server
{
public:
    tcp_server(const std::string&ip,uint16_t& port):ip_(ip),port_(port)
    {
		//创建好套接字
		listen_sock_.Socket();
	}
	~tcp_server()
	{
		listen_sock_.Close();
	}
    //服务器启动函数
	bool start(Handler handler)
	{
		//创建套接字
		//绑定ip和端口号
		listen_sock_.Bind(ip_,port_);
		//监听
		listen_sock_.Listen(12);
		//提取连接
		//创建套接字,ip,port获取发送端的信息
		for(;;)
		{
			//创建套接字,ip和port获取接收端的信息
			//创建通信套接字
			TcpSocket rsocket;
			std::string ip;
			uint16_t port;
    		//提取连接
 			if(!listen_sock_.Accept(&rsocket,&ip,&port))
			{
				continue;
			}
			printf("client的ip地址:%s,端口号:%d\n",ip.c_str(),port);
			//通信
			for(;;)
			{
				std::string req;
				//读取请求,失败就结束循环
				bool ret=listen_sock_.Recv(&req);
 				if(!ret)
				{
					printf("[ client %s %d ] disconnet\n",ip.c_str(),port);
					//关闭rsocket
					rsocket.Close();
					break;
				}
				//接收响应
				std::string resp;
				handler(req,&resp);
				//返回响应
				rsocket.Send(resp);
				printf("[%s %d] rep %s resp% s\n",ip.c_str(),port,req.c_str(),resp.c_str());
			}
	}
	return true;
}
private:
	TcpSocket listen_sock_;
	std::string ip_;
	uint16_t port_;
};
3.5 TCP词典服务器
#include<iostream>
#include"tcp_server.hpp"
#include<map> 
#include<unordered_map>
void translate(const std:: string&req,std::string*resp)
{                       
	auto it=dict.find(req);  
	if(it==dict.end())  
    {  
		*resp="未找到";  
		return ;  
	}  
	*resp=it->second; 
}
//用map存储数据
std::unordered_map<std::string,std::string>dict;
int main(int argc,char*argv[])
{
	if(argc!=3)
	{
	printf("useage:./dict_tcp_server [ip] [port]\n");
	return 1;
	}
	//创建服务器
	dict.insert(std::make_pair("hello", "你好"));
	dict.insert(std::make_pair("world", "世界"));
	dict.insert(std::make_pair("c++", "最好的编程语言"));
	dict.insert(std::make_pair("cxk", "鸡哥"));
	dict.insert(std::make_pair("ikun","爱鲲"));
	dict.insert(std::make_pair("理智","荔枝"));
	dict.insert(std::make_pair("爱好","唱 跳 rap 篮球"));
	dict.insert(std::make_pair("香精煎鱼","想进监狱"));
	Tcpserver server(argv[1], atoi(argv[2]));
	server.start(translate);
	return 0;
}  
3.6TCP通用客户端
#pragma once
#include "tcp_socket.hpp"
class TcpClient 
{
public:
	TcpClient(const std::string& ip, uint16_t port) :ip_(ip), port_(port) 
    {
		sock_.Socket();
	}
	~TcpClient() 
    {
        sock_.Close();
	}
    bool Connect() 
    {
		return sock_.Connect(ip_, port_);
	}
	bool Recv(std::string* buf) 
    {
		return sock_.Recv(buf);
	}
	bool Send(const std::string& buf) 
    {
		return sock_.Send(buf);
	}
private:
	TcpSocket sock_;
	std::string ip_;
	uint16_t port_;
};
3.7TCP词典客户端
#include "tcp_client.hpp"
#include <iostream>
#include<string>
int main(int argc,char*argv[])
{       
	if(argc!=3)
	{     
		printf("useage:./dict_tcp_client [ip] [port]");
		return 1;
	}     
	//创建客户端
	TcpClient client(argv[1], atoi(argv[2]));
	bool ret=client.Connect();
	if(!ret)
	{     
		return 1;
	}     
	for(;;)
	{     
		std::cout<<"请输入你想要查询的单词:"<<std::endl;
		std::string word;
		std::cin>>word;
		if(!std::cin)
		{   
			break;
		}   
		client.Send(word);
		std::string buf;
		client.Recv(&buf);
		std::cout<<word<<": "<<buf<<std::endl;                                       
	}                                                         
	return 0;                                                 
} 

4.实现可以连接多个客服端的服务器

​ 再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信.

​ 分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求。

​ 可以使用多线程服务器,可以实现多个客户端的同时连接。实现的时候,只需要修改服务端即可。

4.1多进程服务端实现
typedef std::function<void (const std::string& req,std::string* resp)> Handler;
// 多进程版本的 Tcp 服务器
class TcpProcessServer 
{
public:
	TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) 
    {
        //创建套接字
        listen_sock_.Socket()
		// 需要处理子进程
         //由内核回收子进程
		signal(SIGCHLD, SIG_IGN);
	}
	void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port,Handler handler) 
	{
		int ret = fork();
		if (ret > 0){
		// 父进程不需要做额外的操作, 直接返回即可.
		// 这里回收子进程不能使用wait,因为wait是阻塞等待的
    	//如果使用 wait , 会导致父进程不能快速再次调用到 accept, 仍然没法处理多个请求
		new_sock.Close();
		return;
		} 
        else if (ret == 0) 
        {
    	// 处理具体的连接过程. 每个连接一个子进程
			for (;;) 
        	{
				std::string req;
				bool ret = new_sock.Recv(&req);
				if (!ret) 
             	 {
					// 当前的请求处理完了, 可以退出子进程了. 注意, socket 的关闭在析构函数中就完成了
					printf("[client %s:%d] disconnected!\n", ip.c_str(), port);
					exit(0);
				}
				std::string resp;
				handler(req, &resp);
				new_sock.Send(resp);
				printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port,req.c_str(), resp.c_str());
			}
        	} 
        else 
        {
			perror("fork");
		}
	}
	bool Start(Handler handler) 
	{
		// 1. 创建 socket;
		// 2. 绑定端口号
		listen_sock_.Bind(ip_, port_);
		// 3. 进行监听
		listen_sock_.Listen(5)
		// 4. 进入事件循环
		for (;;) 
         {
		// 5. 进行 accept
            
         //创建通信套接字
			TcpSocket new_sock;
			std::string ip;
			uint16_t port = 0;
			if (!listen_sock_.Accept(&new_sock, &ip, &port)) 
        	 {
				continue;
			}
			printf("[client %s:%d] connect!\n", ip.c_str(), port);
			ProcessConnect(new_sock, ip, port, handler);
		}
		return true;
	}
private:
	TcpSocket listen_sock_;
	std::string ip_;
	uint64_t port_;
};
4.2多线程服务端实现
#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"
typedef std::function<void (const std::string&, std::string*)> Handler;

//通信套接字接口
struct ThreadArg {
	TcpSocket new_sock;
	std::string ip;
	uint16_t port;
	Handler handler;
};
class TcpThreadServer 
{
public:
	static void* ThreadEntry(void* arg) 
    {
    	// reinterpret_cast:强制类型转换
		ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);
		ProcessConnect(p);
		// 一定要记得释放内存!!! 也要记得关闭文件描述符
		p->new_sock.Close();
		delete p;
		return NULL;
	}
	TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) 
    {
        listen_sock_.Socket();
    }
	bool Start(Handler handler) 
	{
		// 1. 创建 socket;
		// 2. 绑定端口号
		listen_sock_.Bind(ip_, port_)
		// 3. 进行监听
		listen_sock_.Listen(5)
		// 4. 进入循环
		for (;;) 
        {
		// 5. 进行 accept
		ThreadArg* arg = new ThreadArg();
		arg->handler = handler;
    	//6.提取连接,并提取连接的ip port 信息
		bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);
		if (!ret) 
    	{
			continue;	
		}	
		printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);
		// 6. 创建新的线程完成具体操作
		pthread_t tid;
		pthread_create(&tid, NULL, ThreadEntry, arg);
		pthread_detach(tid);
		}
		return true;
	}
	// 处理单次连接. 这个函数也得是 static
	static void ProcessConnect(ThreadArg* arg) 
    {
		// 1. 循环进行读写
		for (;;) 
        {
			std::string req;
		// 2. 读取请求
			bool ret = arg->new_sock.Recv(&req);
			if (!ret) 
             {
				printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);
				break;
			}
			std::string resp;
			// 3. 根据请求计算响应
			arg->handler(req, &resp);
			// 4. 发送响应
			arg->new_sock.Send(resp);
			printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(),arg->port, req.c_str(), resp.c_str());
		}
	}
private:
    //监听套接字
	TcpSocket listen_sock_;
	std::string ip_;
	uint16_t port_;
};

5.TCP通信流程

在这里插入图片描述

服务器初始化:

  • 1.调用监听socket, 创建文件描述符;

  • 2.调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失
    败;

  • 3.调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;

  • 4.调用accecpt,并阻塞, 等待客户端连接过来,获取一个通信套接字;

建立连接的过程:

  • 1.调用socket, 创建文件描述符;
  • 2.调用connect, 向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为:三次握手

数据传输的过程

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据;

  • 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;

  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;

  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求; 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程:

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);

  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);

  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)

  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

    这个断开连接的过程, 通常称为:四次挥手

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

影中人lx

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值