前几天因为一直在看IPC(进程间通信),有几个问题去搜的时候无意中发现这些IPC方法其实以及不大常用了,最最常用的还是socket……
我是一直以为socket只会在网络上使用较多,其他不大会使用,现在发现我可能真的错了,于是打算把学IPC的精力放在巩固和加深socket上,其他IPC只做大概了解。
最基础的 TCP 套接字编程
暂时只贴注释代码
Server
/* ***********************************************************************
> File Name: k_server.cpp
> Author: Key
> Mail: keyld777@gmail.com
> Created Time: Fri 06 Apr 2018 09:50:54 PM CST
*********************************************************************** */
#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#define SERV_PORT 6372
#define LINSTENDQ 1024
#define MAXLINE 4096
typedef struct sockaddr SA;
void str_echo(int);
// server
// socket bind listen accept read/write close
int main()
{
struct sockaddr_in servaddr, cliaddr;
int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/*
* socket 参数: 协议族,套接字类型,协议类型
* 协议族有
* AF_INET IPv4 / AF_INET6 IPv6 / AF_LOCAL Unix域协议 / AF_ROUTE 路由套接字协议 / AF_KEY 密钥协议
* 套接字类型有
* SOCK_STREAM 字节流 / SOCK_DGRAM 数据报 / SOCK_SEQPACKET 有序分组 / SOCK_RAW 原始套接字
* 协议类型有
* IPPROTO_TCP TCP / IPPROTO_UDP UDP / IPPROTO_SCTP SCTP / 0 由前两个参数默认推导的协议类型 / ...
*/
bzero(&servaddr, sizeof servaddr);
servaddr.sin_family = AF_INET;
//inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// INADDR_ANY 0.0.0.0 监听任意ip
// htonl : host to net long 主机数转化为网络字节序
servaddr.sin_port = SERV_PORT; // 选择 端口
bind(listen_fd, (SA*)&servaddr, sizeof servaddr);
/* bind 作用: 将一个本地协议地址赋予给一个套接字
*
* 若不调用bind,ip和端口会在 listen / connect 调用时由内核随即分配,这对服务器来说是不允许的
*/
listen(listen_fd, LINSTENDQ);
/* listen 函数做了两件事
* 1. socket创建的套接字默认是主动连接的套接字,listen将其转化为被动,即监听套接字
* 2. 设置最大连接数量 即第二参数
*
* 关于主动套接字和被动套接字,其明显区别就在于客户端和服务器所绑定的套接字地址结构
* 主动套接字的ip指的是所连接的服务器的ip,端口是服务器的端口
* 被动套接字的ip指的是所接收的客户端的ip,端口是开放的端口
*/
puts("listen...");
while (true) {
socklen_t clilen = sizeof cliaddr;
int connfd = accept(listen_fd, (SA*)&cliaddr, &clilen), child_pid;
/* accept 参数: 套接字描述符 socket地址结构指针 及其 长度
* accept阻塞至收到connect请求
*/
printf("has a client connect : %s\n", inet_ntoa(cliaddr.sin_addr));
if ((child_pid = fork()) == 0) {
close(listen_fd);
str_echo(connfd);
printf("client %s disconnect.\n", inet_ntoa(cliaddr.sin_addr));
exit(0);
}
}
return 0;
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
do {
while ((n = read(sockfd, buf, MAXLINE)) > 0) {
printf("client say : %s", buf);
buf[n - 1] = 0;
strcat(buf, " received!\n");
write(sockfd, buf, strlen(buf));
bzero(buf, MAXLINE);
}
} while (n < 0 && errno == EINTR);
if (n < 0)
fputs("str_echo : read error", stdout);
}
Client
/* ***********************************************************************
> File Name: k_client.cpp
> Author: Key
> Mail: keyld777@gmail.com
> Created Time: Fri 06 Apr 2018 11:29:57 PM CST
*********************************************************************** */
#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define SERV_PORT 6372
#define MAXLINE 4096
#define LISTENDQ 1024
typedef struct sockaddr SA;
void str_cli(FILE* fp, int sockfd);
int main(int argc, char** argv)
{
if (argc != 2) {
fputs("usege : k_client server_ip", stdout);
exit(0);
}
struct sockaddr_in servaddr;
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = SERV_PORT;
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
// 将点分十进制 -> 网络字节序 二进制
// 反操作 inet_ntop
if(connect(sock_fd, (SA*)&servaddr, sizeof servaddr) < 0) {
fputs("connect fail!",stdout);
exit(0);
}
// connect 与 bind 参数一致
str_cli(stdin, sock_fd);
return 0;
}
void str_cli(FILE* fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) != NULL) {
//fputs(sendline, stdout);
write(sockfd, sendline, strlen(sendline));
if (read(sockfd, recvline, MAXLINE) == 0) {
fputs("str_cli : server terminated prematurely", stdout);
exit(0);
}
fputs(recvline, stdout);
bzero(recvline, strlen(recvline));
}
}
epoll 版本
eventspoll,一般来说只会用在服务器端,至少我现在没碰到过客户端的……
这里我更改的也只有服务器端,客户端不变也不贴
/* ***********************************************************************
> File Name: k_server.cpp
> Author: Key
> Mail: keyld777@gmail.com
> Created Time: Fri 06 Apr 2018 09:50:54 PM CST
*********************************************************************** */
#include <arpa/inet.h>
#include <iostream>
#include <netinet/in.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#define SERV_PORT 6372
#define LINSTENDQ 1024
#define MAXLINE 4096
#define EPOLL_MAX_NUM 1024
typedef struct sockaddr SA;
void str_echo(int);
// server
// socket bind listen accept read/write close
int main()
{
struct sockaddr_in servaddr, cliaddr;
int listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bzero(&servaddr, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = SERV_PORT;
bind(listen_fd, (SA*)&servaddr, sizeof servaddr);
listen(listen_fd, LINSTENDQ);
puts("listening...");
int epfd;
struct epoll_event new_event, *events;
if ((epfd = epoll_create(EPOLL_MAX_NUM)) < 0) {
// 创建epoll,参数为最大监听事件数
perror("epoll create");
close(listen_fd);
return 1;
}
new_event.events = EPOLLIN;
new_event.data.fd = listen_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &new_event) < 0) {
/* 参数 epoll描述符 epoll对监听事件的操作 监听的描述符 监听事件
* epoll描述符 由 epoll_create 创建
* 操作 有 EPOLL_CTL_ADD 添加监听事件 / EPOLL_CTL_MOD 修改 / EPOLL_CTL_DEL 删除
* 监听事件 结构如下
* struct epoll_event {
* __uint32_t events;
* // epoll事件 有 EPOLLIN / EPOLLOUT / EPOLLPRI / EPOLLERR / EPOLLHUP / EPOLLET / EPOLLONESHOT ,后文详细解释
* // 常用的大概就是 EPOLLIN / EPOLLOUT / EPOLLET 菜鸟水平如此认为
* epoll_data_t data; //用户传递的数据
* }
*
* typedef union epoll_data {
* void *ptr;
* int fd;
* uint32_t u32;
* uint64_t u64;
* } epoll_data_t;
*/
perror("epoll ctl add listen_fd");
close(epfd), close(listen_fd);
return 1;
}
events = (epoll_event*)malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM);
int client_fd;
char buf[MAXLINE];
while (true) {
int active_fds_cnt = epoll_wait(epfd, events, EPOLL_MAX_NUM, -1);
for (int i = 0; i < active_fds_cnt; i++) {
auto& event = events[i];
if (event.data.fd == listen_fd) {
//此时触发的其实也是 EPOLLIN,个人理解是本地sock_fd要求被绑定
socklen_t clilen = sizeof cliaddr;
if ((client_fd = accept(listen_fd, (SA*)&cliaddr, &clilen)) < 0) {
perror("accept");
continue;
}
char ip[20];
printf("new connection[%s:%d]\n", inet_ntop(AF_INET, &cliaddr.sin_addr, ip, sizeof(ip)), ntohs(cliaddr.sin_port));
//ntohs 16位操作 / ntohl 32位操作
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);
} else if (event.events & EPOLLIN) {
// 监听的套接字被(在此为客户端)写入,即 可读(在此为服务器)的时候,触发 EPOLLIN
printf("EPOLLIN %d\n", i);
client_fd = event.data.fd;
int n = read(client_fd, buf, MAXLINE);
if (n < 0) {
perror("read");
continue;
} else if (n == 0) {
// 客户端关闭时,客户端被关闭时的事件仍然是 EPOLLIN (大概是本地套接字被断开)读不出任何数据即关闭
printf("a connection closed : %d.\n", client_fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &event);
close(client_fd);
} else {
if (buf[n - 1] == '\n')
n--;
buf[n] = '\0';
printf("[read]: %s\n", buf);
event.events = EPOLLOUT;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
/* 修改或者添加 EPOLLOUT 时会立即触发一次 EPOLLOUT 在这之后的触发条件如下
* 1. 某次write,写满了发送缓存区,返回错误 EAGAIN
* 2. 客户端处理了一些数据,又可以继续写了,此时便会触发 EPOLLOUT
*/
}
} else if (event.events & EPOLLOUT) {
printf("EPOLLOUT %d\n", i);
client_fd = event.data.fd;
strcat(buf, " recived!");
write(client_fd, buf, strlen(buf));
printf("[write]: %s\n", buf);
bzero(buf, strlen(buf));
event.events = EPOLLIN;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
}
}
}
close(epfd);
close(listen_fd);
return 0;
}
名字与地址转换 版本
线程版本
关于 epoll
现代网络编程基本上都会使用epoll作为IO复用,与其说是select / poll 的优化,不如说是弥补来的好。
epoll的几个优势如下:
- 监听数量无限制
- 不需要轮询所有监听事件,主要是基于其操作系统支持的I/O事件通知机制
关于比较重要的一个是 水平触发(LT) 和 边缘触发(ET),创建epoll套接字默认是 LT。
ET触发模式意在事件只会触发一次,不论该事件你有没有进行处理,都不会再被触发事件
而LT与之相反。
LT与ET的区别可以用一句话概括:
LT模式下只要socket处于可读状态(添加EPOLLIN事件时)或可写状态(添加EPOLLOUT事件时),就会一直返回其socket。
ET模式下在第一次返回socket后,只有当socket由不可写到可写(添加EPOLLIN事件时)或由不可读到可读(添加EPOLLOUT事件时),才会返回其socket。
比如触发了 EPOLLIN事件,有100kb 数据可读,但我只读出 50 kb ,在LT模式下它仍会触发EPOLLIN事件,而ET模式下则不会了。当然缓存区的数据并不会因为没有读出而清空。
而实际上,对于一个事件的信息,我们当然必须将其处理完全,所以ET模式下在一次事件中就应当处理完所有信息。即
如果读:必须要将缓冲区的内容全部读出,即读到缓冲区空为止
如果写:一直写,知道需写的数据写完或是缓冲区满为止。
而如果是阻塞模式的花,就可能引起其他套接字饿死的情况,随意在ET模式下套接字必须设置成非阻塞模式。
以我的入门水平只能讲这么点了………………
但是如果细究起来回来很多坑点。
其他
今天太晚了,希望明天能写完……