在熟悉Linux Socket编程的时候,照着视频课程敲了一个P2P服务器客户端,实现了两端都能读写操作。
目录
服务器端
服务器端创建socket()
首先创建socket(),socket 文件描述符叫做listen_fd
//创建socket
int listen_fd = socket(PF_INET,SOCK_STREAM,0);
if (listen_fd < 0)
Error("socket()");
socket() RETURN
-1 error
fd value
构建sockaddr_in结构体
//socket address socket地址
struct sockaddr_in addr;
// memset(&addr,0,)
bzero(&addr,sizeof(addr)); //初始化内存
addr.sin_family = AF_INET;
addr.sin_port = htons(23344); // 转网络字节序
addr.sin_addr.s_addr = htonl(INADDR_ANY); //任意地址
设置socket reuseaddr
//设置socket reuseaddr 重复使用
int on = 1;
int optret = setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
if (optret < 0)
Error("setsockopt()");
bind socket
说明:bind socket_fd with address
//bind socket
int bindret = bind(listen_fd,(const sockaddr *)&addr,sizeof(addr));
if (bindret < 0)
Error("bind()");
监听socket fd
//监听 监听队列长度5
int listenret = listen(listen_fd, 5);
if (listenret < 0)
Error("listen()");
accpte() 接受 client 连接
这个地方会阻塞。返回的 conn_fd 就是client端的socket fd
//accpte 接受client 连接 这个地方会阻塞 返回的client socket 的fd 到时候用这个向client去写
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
int conn_fd = accept(listen_fd,(struct sockaddr *)&clientaddr,&clientaddrlen);
if (conn_fd < 0)
Error("accept()");
服务器读写部分
对于读写操作的话,这部分是阻塞的,操作的时候回阻塞对应的进程或者线程。这里我们选择进程方案:一个进程负责写,一个进程负责读。需要指出的是,所操作的fd一定是client端的。
write(conn_fd,sendbuf,sizeof(sendbuf));
int ret = read(conn_fd, recvbuf, sizeof(recvbuf));
读进程(接收进程)
这部分逻辑分支,可以选择一个while死循环,因为read是阻塞操作,在请求读的时候进程是等待状态的。读是从内核数据缓冲区中拷贝数据出来。
char recvbuf[1024] = {0};
while (1)
{
bzero(recvbuf, sizeof(recvbuf));
int ret = read(conn_fd, recvbuf, sizeof(recvbuf));
if (ret == -1)
{
Error("read()");
}
else if (ret == 0)
{
printf("peer close.\n");
break;
}
// ret > 0
fputs(recvbuf,stdout);
}
对端断开连接
当对端(客户端)断开连接,server端read()会返回0,意味着对端断开连接。
此时我们可以终止服务器端进程了。这里我们通过发送SIGUSR1信号的方式来触发进程退出。
写进程(发送进程)
使用一个while+fgets方式,获取stdin的输入,这里fgets其实我理解也是一个阻塞的方式。没有数据就等待,跟C语言中getchar()是一样的。
char sendbuf[1024]= {0};
//从标准输入流中读 也就是键盘输入 size -1 长度到buff buff的空间已经分配好了
//不可能是NULL 所以会一直循环
while ( (fgets(sendbuf,sizeof(sendbuf),stdin)) != NULL)
{
write(conn_fd,sendbuf,sizeof(sendbuf));
bzero(sendbuf,sizeof(sendbuf));
}
服务器退出
这里我们用到了SIGUSR1 用户自定义信号 默认处理:进程终止
当一个进程调用fork时,因为子进程在开始时复制父进程的存储映像,信号捕捉函数的地址在子进程中是有意义的,所以子进程继承父进程的信号处理方式。
但是当子进程调用exec后,因为exec运行新的程序后会覆盖从父进程继承来的存储映像,那么信号捕捉函数在新程序中已无意义,所以exec会将原先设置为要捕捉的信号都更改为默认动作。
接收到SIGUSR1信号,我们就退出进程。
signal(SIGUSR1,SignalHandler); //在主线程设置signal SIGUSR1的handler
signalHandler //记住 签名void SignalHandler(int sig )
void signalHandler(int sig){
printf("recv sig: %d\n",sig);
printf("child process closed.'cause parent process closed.\n"); //打印日志
exit(EXIT_SUCCESS); //进程退出
}
软中断信号
我们平时在程序运行的时候按下ctrl-c、ctrl-z或者kill一个进程的时候,其实都是内核向这个进程发送了一个特定信号,当进程捕获到信号后,进程会被中断并立即跳转到信号处理函数。默认情况下一个程序对ctrl-c发出的信号(SIGINT)的处理方式是退出进程。
收到信号有几种处理办法:
- 处理信号 signalHandler
- 忽略信号
- 保持信号默认操作
客户端
客户端创建socket
int listen_fd = socket(PF_INET,SOCK_STREAM,0);
if (listen_fd < 0)
Error("socket()");
构建sockaddr_in结构体
你要和对方连接,一定是对方的ip和端口
struct sockaddr_in addr;
// memset(&addr,0,)
bzero(&addr,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");// inet_aton("127.0.0.1",&addr.sin_addr);
addr.sin_port = htons(23344);
connect
客户端的操作很简单,就是一个connect(fd,addr)
// listen_fd
int conn = connect(listen_fd, (struct sockaddr *)&addr, sizeof(addr));
if (conn < 0)
Error("connect()");
客户端代码
代码使用多进程的方式,主进程负责recv操作(读),子进程负责send操作(写)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <vector>
#include <sys/epoll.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <iostream>
#include <algorithm>
#include <sys/types.h>
#define SIZE 1024
int main() {
int sock_fd = socket(PF_INET,SOCK_STREAM,0);
assert(sock_fd >0);
struct sockaddr_in addr;
// memset(&addr,0,)
bzero(&addr,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");// inet_aton("127.0.0.1",&addr.sin_addr);
addr.sin_port = htons(23344);
int conn = connect(sock_fd,(struct sockaddr*)&addr,sizeof(addr));
assert(conn >= 0);
printf("ret: %d client IP: %s Port %d create_socket: %d\n",conn, inet_ntoa(addr.sin_addr),ntohs(addr.sin_port),sock_fd);
pid_t pid;
if (0 >(pid = fork())){
perror("fork");
return -1;
}
else if(0 == pid){
char sendbuf[SIZE] = {'0'};
while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL){
write(sock_fd,sendbuf,sizeof (sendbuf));
bzero(sendbuf,sizeof (sendbuf));
}
}
else{
char recvbuf[SIZE] = {'0'};
while (1) {
read(sock_fd, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
}
}
close(sock_fd);
return 0;
}
总结
P2P这个结构可以用来实现,最简单的局域网聊天室的建立。这里时时使用fork()子进程的方式去实现的。其实也可以用多线程的方式去实现。