9.网络编程套接字

  网络编程基础知识

    IP

    端口号

    TCP协议

    UDP协议

    网络字节序

  socket编程接口

    常见API

      sockaddr 结构体

      sockaddr_in 结构体

      in_addr 结构体

  简单的UDP网络程序

  地址转换函数

    字符串转in_addr的函数

    in_addr转字符串的函数

    字符串和in_addr转换

    inet_ntoa函数

  简单的TCP网络程序(CS结构的英译汉)

  将以上英译汉改成多进程版本



网络编程基础知识



IP

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
源IP地址指的就是发送数据包的那个电脑的IP地址;目的IP地址就是想要发送到的那个电脑的IP地址

博客网络1中网络地址管理部分详细介绍了IP地址网络1–>IP



端口号

端口号(port)是传输层协议的内容

  • 端口号是一个2字节16bit的整数;

  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;

  • 一个进程可以绑定多个端口号

  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;并且一个端口号只能被一个进程占用.

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;



TCP协议1

传输层协议、有连接、可靠传输、面向字节流



UDP协议

传输层协议、无连接、不可靠传输、面向数据报



网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分.网络数据流规定为大端字节序

发送方和接收方是通过如下规则保证网络数据传输是大端字节序
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。

  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

主机字节序和网络字节序转换

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用:

#include <arpa/inet.h>
   
uint32_t htonl(uint32_t hostlong);
uintl6_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uintl6_t ntohs(uint16_t netshort); 

库函数做网络字节序和主机字节序的转换

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;

如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。



socket编程接口



常见API
  • 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

    int socket(int domain, int type, int protocol);

  • 绑定端口号 (TCP/UDP, 服务器)

    int bind(int socket, const struct sockaddr *address, socklen_t address_len);

  • 开始监听socket (TCP, 服务器)

    int listen(int socket, int backlog);

  • 接收请求 (TCP, 服务器)

    int accept(int socket, struct sockaddr* address, socklen_t* address_len);

  • 建立连接 (TCP, 客户端)

    int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

socket编程接口请参考博文:TCP socket API…



sockaddr 结构体
struct sockaddr
{
    _SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ 
    char sa_data[14]; /* Address data. */
}

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.

  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;



sockaddr_in 结构体
/* Structure describing an Internet socket address. */ 
struct sockaddr_in
{
    _SOCKADDR_COMMON (sin_);
    in_port_t sin_port; /* Port number. */ 
    struct in_addr sin_addr; /* Internet address.*/
    
    /* Pad to size of 'struct sockaddr'. */ 
    unsigned char sin_zero[sizeof (struct sockaddr) - 
        _SOCKADDR_COMMON_SIZE -
        sizeof (in_port_t) - 
        sizeof (struct in_addr)];
};

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主
要有三部分信息: 地址类型, 端口号, IP地址



in_addr 结构体
/* Internet address. */ 
typedef uint32_t in_addr_t; 
struct in_addr
{
    in_addr_t s_addr;
};

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

sockaddr、sockaddr_in和in_addr结构体之间的关系

在这里插入图片描述

在这里插入图片描述



简单的UDP网络程序

使用UDP实现一个简单的英译汉的功能

封装 UdpSocket
udp_socket.hpp
#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;
	}
	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_;
};
UDP通用服务器
udp_server.hpp
#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();
	}
	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_;
};



实现英译汉服务器
dict_server.c
#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string& req, std::string* resp) {
	auto it = g_dict.find(req);
	if (it == g_dict.end()) {
		*resp = "未查到!";
		return;
	}
	*resp = it->second;
}
int main(int argc, char* argv[]) {
	if (argc != 3) {
		printf("Usage ./dict_server [ip] [port]\n");
		return 1;
	}
	// 1. 数据初始化
	g_dict.insert(std::make_pair("hello", "你好"));
	g_dict.insert(std::make_pair("world", "世界"));
	g_dict.insert(std::make_pair("c++", "最好的编程语言"));
	g_dict.insert(std::make_pair("bit", "特别NB"));
	// 2. 启动服务器
	UdpServer server;
	server.Start(argv[1], atoi(argv[2]), Translate);
	return 0;
}
UDP通用客户端
udp_client.hpp
#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();
	}
	bool RecvFrom(std::string* buf) {
		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_;
};
实现英译汉客户端
udp_client.c
#include "udp_client.hpp"
#include <iostream>
int main(int argc, char* argv[]) {
	if (argc != 3) {
		printf("Usage ./dict_client [ip] [port]\n");
		return 1;
	}
	UdpClient client(argv[1], atoi(argv[2]));
	for (;;) {
		std::string word;
		std::cout << "请输入您要查的单词: ";
		std::cin >> word;
		if (!std::cin) {
			std::cout << "Good Bye" << std::endl;
			break;
		}
		client.SendTo(word);
		std::string result;
		client.RecvFrom(&result);
		std::cout << word << " 意思是 " << result << std::endl;
	}
	return 0;
}



地址转换函数

基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和struct in_addr表示之间转换;



字符串转in_addr的函数:

#include <arpa/inet・h>

int inet__aton(const char *strptrf struct in_addr *addrptr); 
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptrz void *addrptr);



in_addr转字符串的函数:
char *inet_ntoa(struct in_adar inaaar);
const char *inet_ntop(int family, const void *addrptrf char *strptrf size t len);

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void
*addrptr



字符串和in_addr转换
使用struct in_addr
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
	struct in_addr inaddr;
	inet_aton("127.0.0.1", &inaddr);
	uint32_t* ptr = (uint32_t*)(&inaddr);
	printf("addr: %x\n", *ptr);
	printf("addr_str: %s\n", inet_ntoa(inaddr)); 
	return 0;
}

在这里插入图片描述

使用struct sockaddr_in
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
	struct sockaddr_in addr;
	inet_aton("127.0.0.1", &addr.sin_addr);
	uint32_t* ptr = (uint32_t*)(&addr.sin_addr);
	printf("addr: %x\n", *ptr);
	printf("addr_str: %s\n", inet_ntoa(addr.sin_addr)); 
	return 0;
}

在这里插入图片描述



inet_ntoa函数

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是
否需要调用者手动释放呢?

man inet_ntoa

在这里插入图片描述

查看man手册:inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放

后继的调用将会覆盖,我们观察下面例子:

#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
	struct sockaddr_in addrl;
	struct sockaddr_in addr2;
	addrl.sin_addr.s_addr = 0;
	addr2.sin_addr.s_addr = 0xffffffff;
	char* ptrl = inet_ntoa(addrl.sin_addr);
	char* ptr2 = inet_ntoa(addr2.sin_addr);
	printf("ptrl: %s, ptr2 : %s\n", ptrl, ptr2);
	return 0;
}

理论输出结果应该是:

ptrl: 0.0.0.0, ptr2 : 255.255.255.255

但是输出结果是:

ptrl: 255.255.255.255, ptr2 : 255.255.255.255

inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果

如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?

在APUE2中, 明确提出inet_ntoa不是线程安全的函数;
但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

验证一下在自己的机器上inet_ntoa是否会出现多线程的问题
#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) {
		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) {
		char* ptr = inet_ntoa(addr->sin_addr);
		printf("addr2: %s\n", ptr);
	}
	return NULL;
}
int main() {
	pthread_t tid1 = 0;
	pthread_t tid2 = 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_create(&tid2, NULL, Func2, &addr2);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	return 0;
}

以上代码在有的Linux机器上会出现异常情况,使用条件变量改成线程安全的->

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/*初始化条件变量*/
void* Func1(void* p) {
	struct sockaddr_in* addr = (struct sockaddr_in*)p;
	sleep(1);
	while (1) {
	    pthread_mutex_lock(&mutex);/*锁住互斥量*/
        pthread_cond_signal(&cond);/*条件改变,发送信号,通知t_b进程*/
        char* ptr = inet_ntoa(addr->sin_addr);
		printf("addr1: %s\n", ptr);
        pthread_mutex_unlock(&mutex);/*解锁互斥量*/
        sleep(1);
		
	}
	return NULL;
}
void* Func2(void* p) {
	struct sockaddr_in* addr = (struct sockaddr_in*)p;
	sleep(1);
	while (1) {
	    pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);/*互斥变量mutex改变回原样,并等待cond改变*/
        char* ptr = inet_ntoa(addr->sin_addr);
		printf("addr2: %s\n", ptr);
        pthread_mutex_unlock(&mutex);
        sleep(1);
	}
	return NULL;
}
int main() {
	pthread_t tid1 = 0;
	pthread_t tid2 = 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_create(&tid2, NULL, Func2, &addr2);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	return 0;
}
gcc test_inet_ntoa.c -o test_inet_ntoa -lpthread



简单的TCP网络程序(CS结构的英译汉)

使用TCP socket API,实现一个简单的英译汉的功能

封装 TCP socket

tcp_socket.hpp

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
#define CHECK_RET(exp) if (!(exp)) {\
return false;\
}
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 {
		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_;
};
TCP通用服务器

tcp_server.hpp

#pragma once
#include <functional>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string& req, std::string* resp)> Handler;
class TcpServer {
public:
	TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
	}
	bool Start(Handler handler) {
		// 1. 创建 socket;
		CHECK_RET(listen_sock_.Socket());
		// 2. 绑定端口号
		CHECK_RET(listen_sock_.Bind(ip_, port_));
		// 3. 进行监听
		CHECK_RET(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);
			// 6. 进行循环读写
			for (;;) {
				std::string req;
				// 7. 读取请求. 读取失败则结束循环
				bool ret = new_sock.Recv(&req);
				if (!ret) {
					printf("[client %s:%d] disconnect!\n", ip.c_str(), port);
					// [注意!] 需要关闭 socket
					new_sock.Close();
					break;
				}
				// 8. 计算响应
				std::string resp;
				handler(req, &resp);
				// 9. 写回响应
				new_sock.Send(resp);
				printf("[%s:%d] req: %s, resp: %s\n", ip.c_str(), port,
					req.c_str(), resp.c_str());
			}
		}
		return true;
	}
private:
	TcpSocket listen_sock_;
	std::string ip_;
	uint64_t port_;
};
英译汉服务器

tcp_server.c

#include <unordered_map>
#include "tcp_server.hpp"
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string& req, std::string* resp) {
	auto it = g_dict.find(req);
	if (it == g_dict.end()) {
		*resp = "未找到";
		return;
	}
	*resp = it->second;
	return;
}
int main(int argc, char* argv[]) {
	if (argc != 3) {
		printf("Usage ./dict_server [ip] [port]\n");
		return 1;
	}
	// 1. 初始化词典
	g_dict.insert(std::make_pair("hello", "你好"));
	g_dict.insert(std::make_pair("world", "世界"));
	g_dict.insert(std::make_pair("bit", "贼NB"));
	// 2. 启动服务器
	TcpServer server(argv[1], atoi(argv[2]));
	server.Start(Translate);
	return 0;
}
TCP通用客户端

tcp_client.hpp

#pragma once
#include "tcp_socket.hpp"
class TcpClient {
public:
	TcpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
		// [注意!!] 需要先创建好 socket
		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_;
};
英译汉客户端

dict_client.c

#include "tcp_client.hpp"
#include <iostream>
int main(int argc, char* argv[]) {
	if (argc != 3) {
		printf("Usage ./dict_client [ip] [port]\n");
		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 result;
		client.Recv(&result);
		std::cout << result << std::endl;
	}
	return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配

  • 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接;

  • 服务器理论上不是必须调用bind(), 但如果服务器不调用bind()绑定端口号, 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦;



将以上英译汉改成多进程版本

tcp_thread_server.hpp
#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:
	TcpThreadServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
	}
	bool Start(Handler handler) {
		// 1. 创建 socket;
		CHECK_RET(listen_sock_.Socket());
		// 2. 绑定端口号
		CHECK_RET(listen_sock_.Bind(ip_, port_));
		// 3. 进行监听
		CHECK_RET(listen_sock_.Listen(5));
		// 4. 进入循环
		for (;;) {
			// 5. 进行 accept
			ThreadArg* arg = new ThreadArg();
			arg->handler = handler;
			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* ThreadEntry(void* arg) {
		// C++ 的四种类型转换都是什么?
		ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);
		ProcessConnect(p);
		// 一定要记得释放内存!!! 也要记得关闭文件描述符
		p->new_sock.Close();
		delete p;
		return NULL;
	}
	// 处理单次连接. 这个函数也得是 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_;
};

  1. Transmission Control Protocol ↩︎

  2. Advanced Programming in the UNIX Environment ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值