目录
- 网络编程 套接字(socket)
- 1. 认识端口号
- 2. TCP协议
- 3. UDP协议
- 4. 网络字节序列
- 5. 常见的套接字
- 6. socket编程接口
- 6.1 socket常见API
- socket函数
- recvfrom函数
- sendto函数
- read函数 从tcp socket中读取接收数据
- 6.2 sockaddr结构
- 6.3 地址转换函数
- 6.4 udp socket实例
- 本地回环地址 127.0.0.1
- 6.5 tcp实例
网络编程 套接字(socket)
真正的网络通信过程,本质其实是进程间通信。 将数据在主机间转发仅仅是手段,机器收到之后,需要将数据交付给指定的进程。
因此,仅仅有ip地址是无法完成通信的,有了ip地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出,这个数据要给哪个程序进行解析。
IP地址+端口号称为套接字(SRC_IP+SRC_PORT -> 套接字),因此称为套接字编程。
1. 认识端口号
端口号,标识特定主机上的网络进程的唯一性。
端口号(port)是传输层协议的内容。
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址+端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用,一个进程可以绑定多个端口号。
不是所有的进程都需要端口号。
2. TCP协议
TCP(Transmission Control Protocol 传输控制协议)
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
3. UDP协议
UDP(User Datagram Protocol 用户数据报协议)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
4. 网络字节序列
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
unit32_t htonl(unit32_t hostlong); //将32位的长整数从主机字节序转换为网络字节序
unit16_t htons(unit16_t hostshort); //将16位的短整数从主机字节序转换为网络字节序
unit32_t ntohl(unit32_t netlong); //将32位的长整数从网络字节序转换为主机字节序
unit16_t ntohs(unit16_t netshort); //将16位的短整数从网络字节序转换为主机字节序
- h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
5. 常见的套接字
- 域间socket (本主机内进程间通信,也被称为一种基于套接字接口的管道通信策略)
- 原始socket(不常见,通常用来编写工具,允许绕传输层直接使用网络层,或绕过其他直接使用底层)
- 网络socket
理论上,上面三种套接字,是三种应用场景,对应的应该是三套接口。
但实际上为了设计简单化,将所有的接口进行统一。
6. socket编程接口
网络通信的套接字的标准是基于process的。
6.1 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); //backlog:全链接队列长度
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);//成功返回一个整数,表示接受的socket
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 网络发送接口(tcp)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
socket函数
**创建套接字的一个系统调用接口。**属于计算机网络提供的一个系统调用接口,是对传输层做了相关的一层文件系统级别的封装的接口。
成功返回创建的套接字文件描述符,失败返回-1。
- domain:指定协议族,常用的有AF_INET(IPv4)和AF_INET6(IPv6)。(网络通信or本地通信?)
- type:套接字类型,常用的有SOCK_STREAM(流套接字,提供可靠的、面向连接的通信)和SOCK_DGRAM(数据报套接字,提供不可靠的、无连接的通信)。
- protocol:指定协议,一般为0,表示根据domain和type自动选择合适的协议。
recvfrom函数
recvfrom
函数是用于接收数据的系统调用函数,它从udp套接字接收数据,并将其存储到指定的缓冲区中。recvfrom函数的返回值是一个整数,表示接收到的数据的字节数。
- 如果返回值大于0,表示成功接收到数据,并返回接收到的字节数。
- 如果返回值为0,表示对方已经关闭了连接。
- 如果返回值为-1,表示接收数据出现错误,可以通过检查errno来获取具体的错误原因。
sendto函数
发送数据到指定的目标地址
注意这里的addrlen不是指针,所以传的时候不用传地址。
read函数 从tcp socket中读取接收数据
同理,在tcp socket中,使用write进行写。
6.2 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6等,然而,各种网络协议的地址格式并不相同。
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结构体指针做为
参数。基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息:地址类型,端口号,IP地址。
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
6.3 地址转换函数
字符串转in_addr
in_addr 转字符串
其中
inet_pton
和inet_ntop
不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr
。
6.4 udp socket实例
- 下面是一个简单的echo server例子。echo server:client给server发送消息,server原封不动返回
udp_server.hpp
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#define SIZE 1024
class UdpServer{
public:
UdpServer(uint16_t port, std::string ip="0.0.0.0") :_port(port), _ip(ip), _sock(-1)
{}
bool initServer(){
//从这里开始,就是新的系统调用,来完成网络功能
//1.创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if(_sock < 0){
logMessage(FATAL, "%d : %s", errno, strerror(errno));
exit(2);
}
//2. bind:将用户设置的ip和port在内核中和我们当前的进程强关联
// "192.168.1.3" -> 点分十进制字符串风格的IP地址
//每一个区域取值范围是[0-255]:1字节 2^8 -> 4个区域
//点分十进制字符串风格的IP地址 <-> 4字节
struct sockaddr_in local;
bzero(&local, sizeof(local)); //当前字段清零
local.sin_family = AF_INET;//ipv4 协议家族/域
//服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络
local.sin_port = htons(_port); //转参
//1. 同上,点分十进制先要将字符串风格的IP地址-> 4字节IP
//2. 4字节主机序列 -> 网络序列
//有一套接口,可以一次帮我们做完这两件事情
local.sin_addr.s_addr = inet_addr(_ip.c_str());
//更改为从任意IP中获取信息
//local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
logMessage(FATAL, "%d : %s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
//done
return true;
}
void Start(){
//作为一款网络服务器,永远不退出的!所以是一个死循环
//服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机
//echo server:client给我们发送消息,我们原封不动返回
char buffer[SIZE];
for( ; ;){
//注意:
//peer,纯输出型参数
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
//输入:peer缓冲区大小
//输出:实际读到的peer
socklen_t len = sizeof(peer); //输入输出型参数
//start 读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(s > 0){
buffer[s] = 0; //目前数据当做字符串
uint16_t cli_port = ntohs(peer.sin_port); //peer是从网络中来的,需转换
std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的点分十进制字符串风格的IP地址
printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
}
//分析和处理数据, TODO
//end. 写回数据
sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
}
}
~UdpServer()
{
if(_sock >= 0) close(_sock);
}
private:
//一个服务器,一般必须需要ip地址和port
uint16_t _port;
std::string _ip;
int _sock;
};
#endif
udp_server.cc
#include "udp_server.hpp"
#include <memory>
static void usage(std::string proc){
std::cout << "\nUsage: " << proc << "ip port\n" << std::endl;
}
int main(int argc, char *argv[]){
if(argc != 3){
usage(argv[0]);
exit(1);
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));
svr->initServer();
svr->Start();
return 0;
}
完善client端代码。
client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static void usage(std::string proc){
std::cout << "\nUsage: " << proc << "serverIp serverPort\n" << std::endl;
}
int main(int argc, char *argv[]){
if(argc != 3){
usage(argv[0]);
exit(1);
}
//创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
//client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候选择)
std::string message;
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
char buffer[1024];
while(true){
std::cout << "请输入你的信息# ";
std::getline(std::cin, message);
//当client首次发送消息给服务器的时候,OS会自动给client bind它的IP和PORT
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
if(s > 0){
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
close(sock);
return 0;
}
测试结果:
以上就实现了本地间的通信。
通过
netstat
查看网络连接等信息。
本地回环地址 127.0.0.1
127.0.0.1
是一个特殊的 IP 地址,被称为本地回环地址(loopback address)。它通常用于本机测试和网络通信的目的。当你在计算机上使用
127.0.0.1
作为目标地址时,数据将被发送到本机的网络堆栈,而不会通过网络接口发送出去。这使得你可以在本机上模拟网络通信,而无需实际连接到外部网络。本地回环地址
127.0.0.1
在 IPv4 中被保留,而在 IPv6 中,本地回环地址是::1
。在网络编程中,可以使用
127.0.0.1
来测试和调试网络应用程序,或者在本地搭建服务器和客户端进行通信。
- 通过popen模拟shell,client发出命令,server处理并返回执行结果
void Start(){
//作为一款网络服务器,永远不退出的!所以是一个死循环
//服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机
//echo server:client给我们发送消息,我们原封不动返回
char buffer[SIZE];
for( ; ;){
//注意:
//peer,纯输出型参数
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
//输入:peer缓冲区大小
//输出:实际读到的peer
socklen_t len = sizeof(peer); //输入输出型参数
char result[256];
std::string cmd_echo;
//start 读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(s > 0){
buffer[s] = 0; //目前数据当做字符串
if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr){
std::string err_message = "坏人....";
std::cout << err_message << buffer << std::endl;
sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr*)&peer, len);
continue;
}
FILE *fp = popen(buffer, "r");
if(nullptr == fp){
logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
continue;
}
while (fgets(result, sizeof(result), fp) != nullptr)
{
cmd_echo += result;
}
fclose(fp);
}
//分析和处理数据, TODO
//end. 写回数据
// sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);
}
}
}
- 使用多线程实现客户端之间通讯
client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include "thread.hpp"
uint16_t serverport = 0;
std::string serverip;
// int sock = -1;
static void usage(std::string proc){
std::cout << "\nUsage: " << proc << "serverIp serverPort\n" << std::endl;
}
static void *udpSend(void *args){
int sock = *(int*)((ThreadData*)args)->args_; //线程套接字
std::string name = ((ThreadData*)args)->name_; //线程名
std::string message;
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
//不断进行发送
while (true)
{
std::cerr << "请输入你的信息# "; //标准错误 2打印
std::getline(std::cin, message);
if(message == "quit")
break;
//当client首次发送消息给服务器的时候,OS会自动给client bind它的IP和PORT
sendto(sock, message.c_str() , message.size(), 0, (struct sockaddr*)&server, sizeof(server));
}
return nullptr;
}
static void *udpRecv(void *args){
int sock = *(int*)((ThreadData*)args)->args_; //线程套接字
std::string name = ((ThreadData*)args)->name_; //线程名
char buffer[1024];
while(true){
//初始化为0
memset(buffer, 0, sizeof(buffer));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
if(s > 0){
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
}
int main(int argc, char *argv[]){
if(argc != 3){
usage(argv[0]);
exit(1);
}
//创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0){
std::cerr << "socket error" << std::endl;
exit(2);
}
//初始化port和ip
serverport = atoi(argv[2]);
serverip = argv[1];
/*线程编号,回调方式,传入套接字*/
std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void*)&sock));
std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void*)&sock));
// sender->name();
sender->start();
recver->start();
sender->join();
recver->join();
close(sock);
return 0;
}
udp_server.hpp
void Start(){
//作为一款网络服务器,永远不退出的!所以是一个死循环
//服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机
//echo server:client给我们发送消息,我们原封不动返回
char buffer[SIZE];
for( ; ;){
//注意:
//peer,纯输出型参数
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
//输入:peer缓冲区大小
//输出:实际读到的peer
socklen_t len = sizeof(peer); //输入输出型参数
char result[256];
char key[64];
std::string cmd_echo;
//start 读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(s > 0){
buffer[s] = 0; //目前数据当做字符串
uint16_t cli_port = ntohs(peer.sin_port); //peer是从网络中来的,需转换
std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的点分十进制字符串风格的IP地址
//简单的用户托管
snprintf(key, sizeof key, "%s-%d", cli_ip.c_str(), cli_port); // 127.0.0.1-8080
logMessage(NORMAL, "key: %s", key);
auto it = _users.find(key);
if(it == _users.end()){
//not exits
logMessage(NORMAL, "add new user : %s", key);
_users.insert({key, peer});
}
}
for(auto &iter : _users){
/*key,即前面的用ip和port来标识一个用户的身份*/
std::string sendMessage = key;
sendMessage += "#";
sendMessage += buffer; //127.0.0.1-1234# 你好
logMessage(NORMAL, "push message to %s", iter.first.c_str());
sendto(_sock, sendMessage.c_str(), sendMessage.size(),0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
}
}
}
注意:
-
云服务器无法直接绑定公网IP或 非127.0.0.1、 非0.0.0.0这样的IP,即云服务器无法绑定公网IP。
-
对于服务器来讲,不推荐绑定一个确定的IP,推荐使用绑定任意IP的方案。 即让服务器在工作过程中,可以从指定端口中的任意IP中获取数据。
-
无论是多线程读还是写,用的sock都是一个,sock代表一个文件,可以同时进行收发的原因
- UDP是全双工的,可以同时进行收发而不受干扰。
6.5 tcp实例
可以通过对回调函数service的不同实现,来实现server端对client端的通信返回内容。下面是三个对回调函数不同实现的例子。
static void service( ); //简单的echoserver
static void change( ); //对于client发送到server的所有小写字母,转换为大写字母,再返回
static void dictOnline( ); //实现一个小词典
同样,当获取连接成功时,也列举了四种方式进行通信服务。
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <memory>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "ThreadPool/Task.hpp"
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include <netinet/in.h>
// // 接收到客户端数据后进行的处理
static void service(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
// echo server
char buffer[1024];
while (true)
{
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将对应的发过来的数据当作字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;
}
else if (s == 0) // 读取到返回值为0,代表对端关闭连接
{
logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);
break;
}
else // 异常
{
logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));
break;
}
// 读取成功,继续向socket写入
write(sock, buffer, strlen(buffer));
}
close(sock);
}
static void change(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
// 大小写转换
char buffer[1024];
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将对应的发过来的数据当作字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;
std::string message;
char *start = buffer;
while (*start)
{
char c;
if (islower(*start))
c = toupper(*start);
else
c = *start;
message.push_back(c);
start++;
}
write(sock, message.c_str(), message.size());
}
else if (s == 0) // 读取到返回值为0,代表对端关闭连接
{
logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);
}
else // 异常
{
logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));
}
// 读取成功,继续向socket写入
write(sock, buffer, strlen(buffer));
close(sock);
}
static void dictOnline(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
// echo server
char buffer[1024];
static std::unordered_map<std::string, std::string> dict = {
{"apple", "苹果"},
{"bite", "比特"},
{"banana", "香蕉"},
{"hard", "好难啊"}
};
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将对应的发过来的数据当作字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;
std::string message;
auto iter = dict.find(buffer);
if(iter == dict.end()) message = "我不知道...";
else message = iter->second;
write(sock, message.c_str(), message.size());
}
else if (s == 0) // 读取到返回值为0,代表对端关闭连接
{
logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);
}
else // 异常
{
logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));
}
// 读取成功,继续向socket写入
close(sock);
}
// class ThreadData{
// public:
// int _sock;
// std::string _ip;
// uint16_t _port;
// };
class TcpServer
{
private:
const static int gbacklog = 20; // 一般不能太大也不能太小
// static void *threadRoutine(void *args){
// //线程分离
// pthread_detach(pthread_self());
// ThreadData *td = static_cast<ThreadData *>(args);
// service(td->_sock, td->_ip, td->_port);
// delete td;
// }
public:
TcpServer(uint16_t port, std::string ip = "0.0.0.0")
: _listensock(-1), _port(port), _ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool())
{
}
void initServer()
{
// 1. 创建套接字 -- 进程和文件(以进程方式打开了一个文件)
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
logMessage(FATAL, "create socket error%d:%s", errno, strerror(errno));
exit(2);
}
// 打印出创建的套接字,此处为流式
logMessage(NORMAL, "create socket success, listensock: %d", _listensock);
// 2. bind -- 文件 + 网络
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
// local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为tcp是面向连接的,当正式通信的时候,需要先建立连接
if (listen(_listensock, gbacklog) < 0)
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success!");
}
void Start()
{
// signal(SIGCHLD, SIG_IGN); //主动忽略SIGCHLD信号,子进程退出的时候,自动退出僵尸状态
// SIGCHLD:子进程终止或停止时,父进程收到的信号
_threadpool_ptr->run(); // push任务
while (true)
{
// sleep(1);
// 4. 获取链接
struct sockaddr_in src;
socklen_t len = sizeof(src);
//(servicesock)通过每一次获得的新连接进行IO vs (_sock)j监听socket:获取新连接
int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
// 获取连接失败
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d | \n",
servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// version4 --线程池版本
Task t(servicesock, client_ip, client_port, dictOnline);
_threadpool_ptr->pushTask(t);
// version3 --多线程版本
// ThreadData *td = new ThreadData();
// td->_sock = servicesock;
// td->_ip = client_ip;
// td->_port = client_port;
// pthread_t tid;
// pthread_create(&tid, nullptr, threadRoutine, td);
// version2.1 --多进程版,不使用signal
// pid_t id = fork();
// if(id == 0){
// //child
// close(_listensock);
// if(fork() > 0) exit(0); //子进程本身立即退出
// //孙子进程被os接管
// service(servicesock, client_ip, client_port);
// exit(0);
// }
// //parent
// waitpid(id, nullptr, 0); //因为上面的子进程直接退出,所以不会造成阻塞
// close(servicesock);
// version 1 -- 单进程循环版 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// 很显然,是不能够直接被使用的
// service(servicesock, c lient_ip, client_port);
// version 2 -- 多进程版 -- 创建子进程
// 让子进程给新的连接提供服务,子进程能打开父进程曾经打开的文件fd
// pid_t id = fork();
// assert(id != -1);
// if(id == 0){
// //子进程
// //子进程会继承父进程打开的文件与文件fd,因此此时需要关闭不需要的套接字
// close(_listensock);
// service(servicesock, client_ip, client_port);
// exit(0);
// }
// 父进程
// close(servicesock); //不关闭会造成文件描述符泄漏
}
}
~TcpServer() {}
private:
uint16_t _port;
std::string _ip;
int _listensock;
std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};
tcp_client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIP serverPort\n"
<< std::endl;
}
// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
bool alive = false;
int sock = 0;
std::string line;
while (true)
{
if (!alive)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// 此处客户端依然不需要bind,不需要显示的bind,但是一定是需要port
// 需要让OS自动进行port选择
// 要有连接别人的能力
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
{
std::cerr << "connect error" << std::endl;
exit(3);
}
std::cout << "connect success" << std::endl;
alive = true;
}
std::cout << "请输入# ";
std::getline(std::cin, line);
if (line == "quit")
break;
ssize_t s = send(sock, line.c_str(), line.size(), 0);
if (s > 0)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << "server 回显# " << buffer << std::endl;
}
else if (s == 0)
{
alive = false;
close(sock);
}
}
else
{
alive = false;
close(sock);
}
}
return 0;
}