文章目录
- 1. socket编程接口
- 1-1 socket 常见API
- 1-2 sockaddr结构
- 2. 简单的UDP网络程序
- 2-1 日志(固定用法:标准部分+自定义部分)
- 2-2 服务器代码实现
- 1. 框架
- 2. 初始化服务器
- 3. 服务器运行
- 4. 调用服务器封装函数(UdpServer)
- 2-3 客户端代码
- 2-4 结果展示
- 2-5 改进服务器
- 2-6 改进用户
- 1. 封装线程
- 2. 创建两个线程(一个接受信息一个发送信息)
- 2-7 改进后群聊功能展示
- 2-8 总代码链接
- 3. 简单的TCP网络程序
- 3-1 日志(固定用法参考上面2.1)
- 3-2 服务器代码实现
- 1. 框架
- 2. 初始化服务器
- 3. 调用服务器封装函数(TcpServer)
- 4. 服务器运行(铺垫)
- 5. 服务器运行(多进程版)
- 6. 服务器运行(多线程版)
- 3-3 客户端代码
- 3-4 结果展示
- 3-5 总代码链接
1. socket编程接口
1-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);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
1-2 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX Domain
Socket.
各种网络协议的地址格式并不相同;为了让接口统一我们根据前16位作区别(就好像原始模板划分为两个不同类型模块)
- 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 结构
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
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位的整数。
2. 简单的UDP网络程序
2-1 日志(固定用法:标准部分+自定义部分)
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
using std::cout;
using std::endl;
using std::string;
// 日志是有日志级别的
#define DEBUG 0 // 调试
#define NORMAL 1 // 正常
#define WARNING 2 // 警告
#define ERROR 3 // 错误
#define FATAL 4 // 致命错误
static const size_t BUFFER_NUM = 1024;
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"};
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void LogMessage(int level, const char *format, ...) // 可变参数模板
{
char stdBuffer[BUFFER_NUM]; // 标准部分
const time_t timestamp = time(nullptr);
struct tm *L_Time = localtime(×tamp);
string time;
time += "日期-时间:" + std::to_string(L_Time->tm_year+1900) + "/" + std::to_string(L_Time->tm_mon) + "/" + std::to_string(L_Time->tm_mday) + "-" + std::to_string(L_Time->tm_hour) + ":" + std::to_string(L_Time->tm_min) + ":" + std::to_string(L_Time->tm_sec);
std::to_string(L_Time->tm_sec);
snprintf(stdBuffer, sizeof stdBuffer, "[%s][%s]",
gLevelMap[level], time.c_str());
char logBuffer[BUFFER_NUM]; // 自定义部分
va_list args;
va_start(args, format);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
printf("%s%s\n", stdBuffer, logBuffer);
}
2-2 服务器代码实现
1. 框架
#ifndef _UDP_SERVERHPP
#define _UDP_SERVERHPP
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include "Log.hpp"
using std::cout;
using std::endl;
using std::string;
static const size_t SIZE = 1024;
class UdpServer
{
public:
UdpServer(uint16_t port, string ip = "") : _port(port), _ip(ip)
{
}
// 初始化
bool InitServer()
{
}
// 服务器运行
void start()
{
}
~UdpServer()
{
if(_sock>0) close(_sock); // 通过文件描述符关闭文件
}
private:
// 一个服务器,一般必须需要ip地址和port(16位的整数)
uint16_t _port;
string _ip;
int _sock = -1; // 创建 socket 文件描述符 的返回值
};
#endif
2. 初始化服务器
-
- 创建套接字
- 创建套接字
固定用法AF_INET 和 SOCK_DGRAM;最后一个参数(设置成0)
-
- bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
(1)参数 sockfd ,需要绑定的socket。(文件描述符)
(2)参数 addr ,存放了服务端用于通信的地址和端口。
(3)参数 addrlen ,表示 addr 结构体的大小
(4)返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误。
注意:
我们普通用户见得最多的是这种"192.168.110.132"IP地址是点分十进制字符串风格的IP地址每一个区域取值范围是[0-255]: 1字节 -> 4个区域
理论上,表示一个IP地址,其实4字节就够了(减少浪费)
所有在网络上我们需要把点分十进制字符串风格的IP地址 转为4字节再转为网络序列
不过有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据(inet_addr)
用这个INADDR_ANY宏;表示bind任意IP(便于服务器接收数据)
// 初始化
bool InitServer()
{
// 从这里开始,就是新的系统调用,来完成网络功能啦
// 1. 完成套接字创建
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0) // 创建失败
{
LogMessage(FATAL, "%d:%s | %s | %d", errno, strerror(errno), __FILE__, __LINE__);
exit(2);
}
// 2. 绑定(将用户设置的ip和port在内核中和我们当前的进程强关联)
struct sockaddr_in local;
bzero(&local, sizeof local); // 结构体清理(初始化)
local.sin_family = AF_INET; // 协议家族
local.sin_port = htons(_port); // 网络字节序大端存储
// 将点分十进制字符串风格的IP地址 -> 4字节-> 网络序列
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 | %s | %d", errno, strerror(errno), __FILE__, __LINE__);
exit(3);
}
LogMessage(NORMAL, "init udp server done ... | %s | %d", __FILE__, __LINE__);
return true;
}
3. 服务器运行
recvfrom () 用来接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间,参数len为可接收数据的最大长度
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socklen_t *fromlen);
(1)ssize_t 相当于 long int,socklen_t 相当于int
(2)sockfd:标识一个已连接套接口的描述字。
(3)buf:接收数据缓冲区。
(4)len:缓冲区长度。
(5)flags:调用操作方式。是以下一个或者多个标志的组合体,可通过“ | ”操作符连在一起(通常设置为0)
(6)from 是一个指向sockaddr结构体类型的指针;
(7)*fromlen表示my_addr结构的长度,可以用sizeof操作符获得。
(8)返回值:成功则返回接收到的字符数,失败返回-1.
sendto() 用来将数据由指定的socket传给对方主机。参数s为已建好连线的socket,如果利用UDP协议则不需经过连线操作。参数msg指向欲连线的数据内容,参数flags 一般设0
int sendto ( socket s , const void * msg, int len, unsigned int flags, const
struct sockaddr * to , int tolen ) ;
(1)s:一个用于标识已连接套接口的描述字。
(2)buf:包含待发送数据的缓冲区。
(3)len:缓冲区中数据的长度。
(4)flags:调用执行方式。
(5)to 是一个指向sockaddr结构体类型的指针;
(5)参数tolen表示to结构的长度,可以用sizeof操作符获得。
(6)成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
- 代码块:
// 服务器运行
void start()
{
// 作为一款网络服务器,永远不退出的!
// 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
char buffer[SIZE]; // 存储读取到的数据
for (;;)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof peer);
socklen_t len = sizeof peer;
// 读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0) // 读取成功
{
// 谁发的数据
buffer[s] = 0; // 我们目前数据当做字符串
uint16_t cliPort = ntohs(peer.sin_port); // 从网络中来的
// 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
string cliIP = inet_ntoa(peer.sin_addr);
printf("[%s:%d]# %s\n", cliIP.c_str(), cliPort, buffer);
}
// end. 写回数据
sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
}
}
4. 调用服务器封装函数(UdpServer)
#include "udp_server.hpp"
#include <memory>
static void Usage(string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
// ./udp_server 127.0.0.1 8080
int main(int argc, char *argv[]) // 命令行参数
{
if (argc != 3)
{
Usage(argv[0]); // 使用手册
exit(1);
}
std::string ip = argv[1];
u_int16_t port=atoi(argv[2]);
std::unique_ptr<UdpServer> server(new UdpServer(port, ip)); // 智能指针
server->InitServer();
server->start();
return 0;
}
2-3 客户端代码
- 注意:
(1)client要bind,但是一般client不会显示的bind,程序员不会自己bind
(2)client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port; 就芭比Q了
(3)client一般不需要显示的bind指定port,而是让OS自动随机选择(具体在当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT就是调用sendto()函数自动bind)
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using std::cout;
using std::endl;
using std::string;
static const size_t SIZE = 1024;
static void Usage(string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
// ./udp_client 127.0.0.1 8080
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 << "client: socket error" << endl;
exit(2);
}
// client要要,但是一般client不会显示的bind,程序员不会自己bind
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[SIZE];
while (true)
{
std::cout << "请输入你的信息# ";
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);
struct sockaddr_in temp;
socklen_t len = sizeof temp;
memset(&temp, 0, len);
ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
cout << "server echo# " << buffer << endl;
}
std::cerr << "client: srecvfrom error" << endl;
}
close(sock);
return 0;
}
2-4 结果展示
2-5 改进服务器
- 我们不直接绑定服务器;达到如下效果
- 我们需要哈希桶完成映射;并存储用户信息
// 服务器运行
void start()
{
// 作为一款网络服务器,永远不退出的!
// 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
char buffer[SIZE]; // 存储读取到的数据
for (;;)
{
char key[64];
struct sockaddr_in peer;
memset(&peer, 0, sizeof peer);
socklen_t len = sizeof peer;
// 读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0) // 读取成功
{
// 谁发的数据
buffer[s] = 0; // 我们目前数据当做字符串
uint16_t cliPort = ntohs(peer.sin_port); // 从网络中来的
// 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
string cliIP = inet_ntoa(peer.sin_addr);
// printf("[%s:%d]# %s\n", cliIP.c_str(), cliPort, buffer);
snprintf(key, sizeof(key), "%s-%u", cliIP.c_str(), cliPort); // 127.0.0.1-8080
LogMessage(NORMAL, "key: %s | %s | %d", key, __FILE__, __LINE__);
auto it = _users.find(key);
if (it == _users.end())
{
// 储存
LogMessage(NORMAL, "add new user : %s | %s | %d", key, __FILE__, __LINE__);
_users[key] = peer;
}
}
// end. 写回数据
for (auto &iter : _users)
{
string sendMessage = key;
sendMessage += "# ";
sendMessage += buffer; // 127.0.0.1-1234# 你好
LogMessage(NORMAL, "push message to %s | %s | %d", iter.first.c_str(), __FILE__, __LINE__);
sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
}
}
}
2-6 改进用户
多线程跑起来;实现群聊功能
1. 封装线程
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
using std::cout;
using std::endl;
using std::string;
static const size_t NAME_NUM = 1024;
typedef void*(*fun_t)(void*); // 线程执行的方法
class ThreadDate
{
public:
void *_args;
string _name;
};
class Thread
{
public:
Thread(int num, fun_t callback, void *args) : _func(callback)
{
char nameBuffer[NAME_NUM];
snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
_tdata._name = nameBuffer;
_tdata._args = args;
}
void start() // 创造线程
{
pthread_create(&_tid, nullptr, _func, (void *)&_tdata);
}
void join() // 等待线程
{
pthread_join(_tid, nullptr);
}
string &name() // 线程名字
{
return _tdata._name;
}
private:
fun_t _func;
ThreadDate _tdata;
pthread_t _tid;
};
2. 创建两个线程(一个接受信息一个发送信息)
- 其实稍微把发送和接受解耦;封装一下就可以了
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "thread.hpp"
using std::cout;
using std::endl;
using std::string;
uint16_t serverPort = 0;
string serverIp;
static const size_t SIZE = 1024;
static void Usage(string proc)
{
cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< endl;
}
static void *UdpSend(void *args) // 发送消息
{
ThreadDate *td = (ThreadDate *)args;
int sock = *(int *)td->_args;
string name = td->_name;
// client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
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 << "请输入你的信息# ";
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 *UdpAccept(void *args) // 接受信息
{
ThreadDate *td = (ThreadDate *)args;
int sock = *(int *)td->_args;
string name = td->_name;
char buffer[SIZE];
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof temp;
memset(&temp, 0, len);
ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
cout << buffer << endl;
}
else
{
std::cerr << "client: srecvfrom error" << endl;
}
}
return nullptr;
}
// ./udp_client 127.0.0.1 8080
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 << "client: socket error" << endl;
exit(2);
}
serverIp = argv[1];
serverPort = atoi(argv[2]);
std::unique_ptr<Thread> Sender(new Thread(1, UdpSend, (void *)&sock));
std::unique_ptr<Thread> Accepter(new Thread(2, UdpAccept, (void *)&sock));
// 创建线程
Sender->start();
Accepter->start();
// 等待进程
Sender->join();
Accepter->join();
close(sock); // 关闭文件(sock本质是一个文件)
return 0;
}
2-7 改进后群聊功能展示
注意:
- 首次发消息;也再做管理该用户
2-8 总代码链接
链接:
https://gitee.com/ding-xushengyun/linux__cpp/commit/6bfc59b29aae02abecb88d43b1f2b7b4c071fe2c
3. 简单的TCP网络程序
3-1 日志(固定用法参考上面2.1)
3-2 服务器代码实现
1. 框架
- 框架跟UDP几乎相同
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"
using std::cout;
using std::endl;
using std::string;
static const size_t SIZE = 1024;
class UdpServer
{
const static int gbacklog = 20;
public:
UdpServer(uint16_t port, string ip = "") : _port(port), _ip(ip)
{
}
// 初始化
bool InitServer()
{
}
// 服务器运行
void start()
{
}
~UdpServer()
{
}
private:
// 一个服务器,一般必须需要ip地址和port(16位的整数)
uint16_t _port;
string _ip;
int _listenSock = -1; // 监听套接字(建立新链接---客户)
};
2. 初始化服务器
- 初始化:1. 创建套接字(跟上面UDP几乎相同;不同的是第二参数UDP是数据报,我们是面向字节流) 2. 绑定bind;最大不同TCP只不过多了一个聆听- - -listen
- 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接(就好像厨师一直在等待顾客的到来;因为我们不知道顾客什么适合来吃饭)
- 云服务器不能绑定公有IP;需要随机绑定INADDR_ANY
int listen(int sockfd, int backlog);
(1)sockfd 参数表示监听的 socket 句柄
(2)backlog 参数表示接收请求队列的长度(不能太大)
(3)成功则返回 0,失败返回-1,错误原因存于errno 中。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
- 代码块:
void InitServer()
{
// 1. 创建套接字
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0) // 创建失败
{
LogMessage(FATAL, "创建套接字失败 %d:%s", errno, strerror(errno));
exit(2);
}
LogMessage(FATAL, "创建套接字成功, listensock: %d", _listenSock); // 3
// 2. bind 绑定 文件+网络
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
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, "绑定失败, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
if (listen(_listenSock, gbacklog) < 0)
{
LogMessage(ERROR, "建立链接失败, %d:%s", errno, strerror(errno));
exit(4);
}
LogMessage(NORMAL, "初始化成功!!!");
}
3. 调用服务器封装函数(TcpServer)
- 跟UDP没什么不同
#include <memory>
#include "tcp_server.hpp"
static void Usage(string proc)
{
std::cout << "\nUsage: " << proc << " port\n"
<< std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]); // 使用手册
exit(1);
}
uint16_t port=atoi(argv[1]);
std::unique_ptr<TcpServer> sve(new TcpServer(port));
sve->InitServer();
sve->start();
return 0;
}
4. 服务器运行(铺垫)
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
(1)sock 为服务器端套接字
(2)addr 为 sockaddr_in 结构体变量
(3)addrlen 为参数 addr 的长度,可由 sizeof() 求得。
(4)成功则返回 socket(这个sock用来提供服务的),失败返回-1,错误原因存于errno 中。
tcp 面向字节流而且sock还是文件描述符;我们可以把接受和发送数据当作文件处理(read和write)
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
char buffer[SIZE];
for (;;)
{
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof buffer - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
cout << clientip << ":" << clientport << "# " << buffer << endl;
}
else if (s == 0) // 对写端关闭连接
{
LogMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
break;
}
else
{ //
LogMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
}
- 我们链接(accept)成功后;直接调用service()函数我们得到的只是一个单进程版;什么意思呐?如下图:
(用telnet简单测试一下服务器)我们明显发现只能一个客户发消息;其它客户发消息需要等第一个客户退出才能行。
5. 服务器运行(多进程版)
怎么实现服务器并发处理客户端呐?
- 我们需要让子进程提供服务(调用service函数);父进程监听socket
- 子进程继承父进程的页表;父进程和子进程需要关闭相应文件(子进程:close(_listenSock); 父进程: close(serviceSock);)
- 子进程退出会产生僵尸问题(我们需要父进程不阻塞式处理)
- a. 信号捕捉;主动忽略(signal(SIGCHLD, SIG_IGN);)
- b. 子进程再fork() ,子进程退出;子进程的子进程执行服务(它退出后形成孤儿进程被Init 1一号进程领养)
我们采用信号捕捉的方法
void start()
{
// 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
signal(SIGCHLD, SIG_IGN);
for (;;)
{
// 4. 获取连接
struct sockaddr_in src;
memset(&src, 0, sizeof src);
socklen_t len = sizeof src;
// 获取新连接
int serviceSock = accept(_listenSock, (struct sockaddr *)&src, &len);
if (serviceSock < 0)
{
LogMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t clientPort = ntohs(src.sin_port);
string clientIp = inet_ntoa(src.sin_addr);
LogMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",
serviceSock, clientIp.c_str(), clientPort);
// 开始进行通信服务啦
// version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// service(serviceSock, clientIp, clientPort);
// version 2.0 -- 多进程版 --- 创建子进程
// 让子进程给新的连接提供服务,子进程能打开父进程曾经打开的文件fd 1 0
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
// 子进程:提供服务 不需要监听socket
close(_listenSock);
service(serviceSock, clientIp, clientPort);
exit(0); // 子进程退出会产生僵尸问题;需要主动忽略信号来解决
}
close(serviceSock);
}
}
- telnet简单测试服务器
6. 服务器运行(多线程版)
class ThreadData
{
public:
int _sock;
std::string _ip;
uint16_t _port;
};
- 多线程代码如上所示
- 在多线程这里不用进程关闭特定的文件描述符;因为Linux下的同一个进程中线程共用一张页表
3-3 客户端代码
- TCP和UDP服务器代码都不需要显示绑定bind
- tcp需要链接服务器( connect())
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(1)sockfd:标识一个套接字。
(2)serv_addr:套接字s想要连接的主机地址和端口号。
(3)addrlen:name缓冲区的长度。
(4)如果链接或者绑定成功则返回 0,失败返回-1,错误原因存于errno 中。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
(1)sockfd是socket()的返回值,文件描述符
(2)buf是接受数据的缓存区的指针
(3)len是发送数据的长度
(4)flags标志位,默认为0。
(5)返回值:成功则返回接收到的字符数,失败返回-1.
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
(1)sockfd是socket()的返回值,文件描述符
(2)buf是接受数据的缓存区的指针
(3)len是发送数据的长度
(4)flags标志位,默认为0。
(5)返回值:成功则返回接收到的字符数,失败返回-1.
- (长链接)代码块:
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"
using std::cout;
using std::endl;
using std::string;
static const size_t SIZE = 1024;
static void Usage(string proc)
{
std::cout << "\nUsage: " << proc << " clientIP port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]); // 使用手册
exit(1);
}
string ipClient = argv[1];
uint16_t portClient = atoi(argv[2]);
// 创建字节套接字
int sockClient = socket(AF_INET, SOCK_STREAM, 0);
if (sockClient < 0) // 创建失败
{
std::cerr << "client: 创建套接字失败 " << endl;
exit(2);
}
// 不需要显示bind
// 但需要建立链接
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(portClient);
server.sin_addr.s_addr = inet_addr(ipClient.c_str());
if (connect(sockClient, (struct sockaddr *)&server, sizeof server) < 0)
{
std::cerr << "client: 建立链接失败 " << endl;
exit(3);
}
// 建立链接成功
string line;
while (true)
{
// 通信
cout << "请输入# ";
std::getline(std::cin, line);
if (line == "quit")
break;
send(sockClient, line.c_str(), line.size(), 0); // write(向sockClient中写数据); 发送数据
char buffer[SIZE];
ssize_t s = recv(sockClient, buffer, sizeof(buffer) - 1, 0); // read; 接受数据
if (s > 0)
{
buffer[s] = 0;
cout << "server 回显# " << buffer << endl;
}
else if (s == 0) // 对写端关闭连接
{
cout << "shutdown, me too!" << endl;
break;
}
else
{
std::cerr << "recv socket error" << endl;
break;
}
}
return 0;
}
- 短连接代码块:
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"
using std::cout;
using std::endl;
using std::string;
static const size_t SIZE = 1024;
static void Usage(string proc)
{
std::cout << "\nUsage: " << proc << " clientIP port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]); // 使用手册
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
bool alive = false;
int sock = -1;
string line;
while (true)
{
if (!alive)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// client 要不要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 << "client: 创建套接字失败 " << endl;
exit(2);
}
cout << "链接成功" << endl;
alive = true;
}
// 链接上服务器
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);
buffer[s] = 0;
cout << "server 回显# " << buffer << std::endl;
}
else if (s == 0)
{
cout << "shutdown, me too!" << endl;
alive = false;
close(sock);
}
else
{
std::cerr << "recv socket error" << endl;
alive = false;
close(sock);
}
}
return 0;
}
长链接和短链接在线程池里结果更明显
3-4 结果展示
- 多进程版服务器群聊功能
- 多线程版服务器群聊功能
3-5 总代码链接
链接:
https://gitee.com/ding-xushengyun/linux__cpp/tree/master/tcp