网络应用程序设计模式
C/S——clien/server
- 优点——协议灵活,可以缓存数据
- 缺点——对用户安全造成威胁,开发工作量大
B/S-——browser/server
- 优点——跨平台
- 只能用http
socket编程
什么是socket
- 网络通信的函数接口
- 封装了传输层协议
socket通信
- 服务端
- 客户端
socke编程实际上就是网络IO编程,因此需要读写操作
- read/write
- 文件描述符
- 创建一个套接字,得到的是文件描述符
套接字
- 创建成功,得到一个文件描述符 fd
- fd操作的是一块内核缓冲区
ip地址转换函数
本地IP转网络字节序的函数
int inet_pton(int af, const char *src, void *dst);
- af 地址族协议
- src 点分十进制的IP
- 转化的结果存入第三个参数——dst 所指的那块内存
网络字节序转本地IP int—>字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
服务端创建连接的过程
第一步——创建套接字——>得到一个文件描述符(int lfd=socket)
第二步——绑定本地IP和端口(bind(lfd, &serv, sizeof(serv))函数
绑定的时候要注意结构体 struct sockarr_in serv 结构体绑定初始化
- serv.port=htons(port); 端口初始化
- serv.IP=htonl(INADDR_ANY); 通过宏适配IP
绑定的时候,端口和IP都需要做一个小端转大端的操作,接受和发送数据的时候不需要做这些。
第三步——监听
listen(lfd,128);//128是同一时间的最大连接数
第四步——等待并接受连接请求
- struct socketaddr_in client;
- int sizeof(client);
- int cfd=accept(lfd, &client, &len);
- cfd——用于通信,是accept函数的返回值。read和write都需要使用cfd的描述符,而不是监听的描述符。
- lfd——用于监听有没有人连接到我这个服务器。
要保存连接到这个服务器的客户端的IP和端口
第五步——通信
- 接收数据:read / recv
- 发送数据:write / send
第六步——断开
- close(lfd)
- close(cfd)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
int main(int argc, const char* argv[]){
// 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket error");
exit(1);
}
// lfd 和本地的IP port绑定
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET; // 地址族协议 - ipv4
server.sin_port = htons(8888);
server.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server));
if(ret == -1){
perror("bind error");
exit(1);
}
// 设置监听
ret = listen(lfd, 20);
if(ret == -1){
perror("listen error");
exit(1);
}
// 等待并接收连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int cfd = accept(lfd, (struct sockaddr*)&client, &len);
if(cfd == -1){
perror("accept error");
exit(1);
}
printf(" accept successful !!!\n");
char ipbuf[64] = {0};
printf("client IP: %s, port: %d\n",
inet_ntop(AF_INET, &client.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(client.sin_port));
// 一直通信
while(1) {
// 先接收数据
char buf[1024] = {0};
int len = read(cfd, buf, sizeof(buf));
if(len == -1) {
perror("read error");
exit(1);
}
else if(len == 0) {
printf(" 客户端已经断开了连接 \n");
close(cfd);
break;
}
else {
printf("recv buf: %s\n", buf);
// 转换 - 小写 - 大写
for(int i=0; i<len; ++i) {
buf[i] = toupper(buf[i]);
}
printf("send buf: %s\n", buf);
write(cfd, buf, len);
}
}
close(lfd);
return 0;
}
客户端创建连接的过程
第一步——创建套接字
int fd=socket
第二步——连接服务器
- struct socketaddr_in server; //server端的IP和端口信息
- server.port
- server.ip=(int) 重点是怎么把ip地址转成十进制
- server.family
- int cfd=connect( fd, &client, &len);
第三步——通信
- 接收数据:read / recv
- 发送数据:write / send
第四步——断开
- close(fd)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
//创建套接字需要包含的头文件
#include<arpa/inet.h>
int main(int argc, const char* argv[]){
if(argc<2){
printf("eg:./a.out port\n");
exit(1);
}
int port=atoi(argv[i]);
//创建套接字
//int socket(int domain(地址族协议), int type, int protocol);
//协议protocol我们这里就默认0就行了
int fd=socket(AF_INET, SOCK_STREAM, 0);
//链接服务器_初始化IP和端口号
struct sockarrd_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family=AF_INET;
//端口自需要做一个转换
serv.sin_port=htons(port);
//由于htonl()函数里面只能放整型,
//因此用以下函数来做一下点分十进制转网络字节序的int型
//第一个参数是地址族,第二个参数是点分十进制IP,
//第三个参数是指向一块内存的指针
inet_pton(AF_INET, "127.0.0.1",&serv.sin_addr.s_addr);
connect(fd, (struct sockarrd*)&serv, sizeof(serv));
//通信
while(1){
//发送数据
char buf[1024];
printf("请输入要发送的字符串\n");
//从终端接受输入 fgets是一个阻塞函数
//第一个参数指向buf,第二个参数是buf的大小
//char *fgets(char*s, int size, FILE *stream);
fgets(buf, sizeof(buf), stdin);
write(fd, buf, strlen(buf));
//等待接受数据
int len=read(fd, buf, sizeof(buf));
if(len==-1){
perror("read error");
exit(1);
}else if(len==0){
//当对端关闭连接之后,read动作就不再阻塞了
printf("服务器端关闭了链接\n");
break;
}else{
//把读取到的数据打印到终端上
printf("recv buf:%s\n", buf);
}
}
close(fd);
return 0;
}
socket函数封装
小技巧:man手册跳转的时候不区分大小写,你想封装一个函数,只要把函数名的大小写变动一下,其余的不变动,也可以通关man手册来查看被封装的函数的man手册。
我们做函数封装的时候就不用再做错误判断了,因为函数的内部应处理完了。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s){
perror(s);
exit(-1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr){
int n;
again:
//也就是n ==-1 accept是一个阻塞函数
//当accept函数被信号阻塞之后,立即去处理信号
//处理完信号之后不能再继续阻塞了,他返回 -1
if ((n = accept(fd, sa, salenptr)) < 0) {
//ECONNABORTED 发生在重传(一定次数)失败后,强制关闭套接字
//EINTR 说明函数在阻塞的过程中,被信号中断
//通过捕捉使得accept函数的返回值为 -1 的信号来分析中断的原因
//这段代码可以改成while,他用了if和goto的组合
if ((errno == ECONNABORTED) || (errno == EINTR)){
goto again;
}else{
perr_exit("accept error");
}
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen){
int n;
if ((n = bind(fd, sa, salen)) < 0){
perr_exit("bind error");
}
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen){
int n;
n = connect(fd, sa, salen);
if (n < 0) {
perr_exit("connect error");
}
return n;
}
int Listen(int fd, int backlog){
int n;
if ((n = listen(fd, backlog)) < 0){
perr_exit("listen error");
}
return n;
}
int Socket(int family, int type, int protocol){
int n;
if ((n = socket(family, type, protocol)) < 0)
{
perr_exit("socket error");
}
return n;
}
//进行套接字通信的时候,read会阻塞,他阻塞是因为read指向文件描述符fd,fd指向一个设备
//文件描述符实际上操作的是一个设备。文件描述符有时候会阻塞,有时候不会阻塞
ssize_t Read(int fd, void *ptr, size_t nbytes) {
ssize_t n;
again:
//判断read阻塞读的时候是不是被信号中断了
//如果是被信号中断了,就让他继续read
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
//写的时候也会阻塞,当write在写的时候,如果缓冲区满了,就不能再写了
//就需要阻塞等待
ssize_t Write(int fd, const void *ptr, size_t nbytes){
ssize_t n;
again:
if ((n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd) {
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
/*参三: 应该读取的字节数*/
//socket 4096 readn(cfd, buf, 4096) nleft = 4096-1500
//一次性王buf里面读n个
//读不够,你就等着
ssize_t Readn(int fd, void *vptr, size_t n) {
size_t nleft; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
char *ptr;
ptr = vptr;
nleft = n; //n 未读取字节数
//为什么循环读?因为一次性可能读不够
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR){
nread = 0;
}else{
return -1;
}
}
else if (nread == 0){
break;
}
//剩余的还没装满的空间
nleft -= nread; //nleft = nleft - nread
//数据要读到vptr里面去 ptr = vptr; 可以使得指针移动
//防止新读入的数据被覆盖
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n){
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
//my_read每调用一次,他就读一个字节,他直到把100个字节读完之后,才会再去读一百个字节
static ssize_t my_read(int fd, char *ptr) {
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
//直到read_cnt减为零之后,他才会再去读一次,一次读一百个
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){
if (errno == EINTR)
goto again;
return -1;
}
else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
/*readline --- fgets*/
//传出参数 vptr
//读一行
ssize_t Readline(int fd, void *vptr, size_t maxlen){
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
//通过一个字符一个字符的去判断
for (n = 1; n < maxlen; n++) {
//my_read函数原型在上面
if ((rc = my_read(fd, &c)) == 1){
*ptr++ = c;
//找到 \n,就说明读到了一行
if (c == '\n')
break;
}
else if (rc == 0) {
*ptr = 0;
return n-1;
}
else
return -1;
}
*ptr = 0;
return n;
}
tcp三次握手
1000是随机产生的序号,0是我们携带的数据
TCP数据传输
四次挥手
第一次挥手时候FIN携带的序号是对方最后一次ACK携带的序号。
第三次挥手时候,服务端发送的FIN携带的序号等于是客户端最后一次给服务器的ACK值。
滑动窗口
mss最大的数据量
数据重传的机制是靠ACK来保障的,通过ACK我就可以知道哪些数据i收到了。
多进程并发服务器
父进程——等待接受连接请求
子进程——每有一个新的连接请求,就产生一个新的子进程去提供服务
一个进程中的文件描述符是有上限的,默认的上限是1024个。
读时共享,写时复制的原理
父子进程有一个共享数据a,初始时,两个进程都读橙色的圈圈,之后父进程把a的值做了修改,于是每次他都读粉色的圈圈,后来子进程也把a修改了,于是他只读黄色的圈圈。
多进程伪代码
//回收函数
void recyle(int num) {
while(waitpid(-1, NULL, wnohang) > 0);
}
int main() {
// 监听
int lfd = sock();
// 绑定
bind();
// 设置监听
listen();
//创建子进程之前做了一个信号捕捉,因此这个信号捕捉会被子进程继承
// 信号回收子进程
struct sigaction act;
act.sa_handler = recyle;
act.sa_falgs = 0;
//在处理捕捉到的信号的过程中,你想屏蔽某个信号,就把他设置在mask里面
//如果你不想屏蔽某些信号了,你就需要把它做一个清空操作
//由于act是一个局部变量,他里面有什么我们不知道,因此需要对mask进行初始化
sigemptyset(&act.sa_mask);
//设置要捕捉的信号
sigaction(SIGCHLD, &act, NULL);
// 父进程
while(1) {
int cfd = accept();
// 创建子进程
pid_t pid = fork();
// 子进程
if(pid == 0) {
close(lfd);
// 通信
while(1) {
int len = read();
if(len == -1) {
exit(1);
}
else if(len == 0) {
close(cfd);
break;
}
else {
write();
}
}
// 退出子进程
return 0; //exit(1);
} else {
// 父进程
close(cfd);
}
}
}
多进程服务端的实现例子
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
//用于进程回收的回调函数
void recyle(int num){
pid_t pid;
while((pid=waitpid(-1, NULL, WNOHANG))>0){
printf("child died, pid=%d\n", pid);
}
}
int main(int argc, char *argv[]){
if(argc<2){
printf("eg: ./a.out port \n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len=sizeof(serv_addr);
int port=atoi(argv[1]);
//创建套接字
int lfd=socket(AF_INET, SOCK_STREAM, 0);
//初始化服务器
memset(&serv_addr, 0, serv_len);
//地址族
serv_addr.sin_family=AF_INET;
//监听本地所有IP
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//设置端口
serv_addr.sin_port=htons(port);
//绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
//设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept .... \n");
//使用信号回收子进程pcb
//不然让父进程一直用wait循环等待,那父进程就什么都做不了了
struct sigaction act;
act.sa_handler=recyle;
act.sa_flags=0;
//初始化mask,不然局部变量里面有什么我们不确定
sigemptyset(&act.sa_mask);
//规定我们要接受的信号
sigaction(SIGCHLD, &act, NULL);
struct sockaddr_in client_addr;
socklen_t cli_len=sizeof(client_addr);
while(1){
//父进程需要接受连接请求
//没有连接,那就阻塞着
int cfd=accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd==-1){
perror("accept error");
exit(1);
}
printf("connect successful \n");
//创建子进程
pid_t pid=fork();
if(pid==0){
//关闭监听描述符
close(lfd);
char ip[64];
//子进程与客户端进程通信
while(1){
//当我接收到一条信息,我就打印对方的端口和ID
//inet_ntop,第二个参数是大端的整型IP,
//写在第三个参数指向的地址
//ntohs
printf("client IP: %s, port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
ip, sizeof(ip)), ntohs(client_addr.sin_port));
char buf[1024];
int len=read(cfd, buf, sizeof(buf));
if(len==-1){
perror("read error");
exit(1);
}else if(len ==0){
printf("客户端断开了连接\n");
close(cfd);
break;
}else{
//把接受到的数据进行打印
printf("recv buf: %s\n", buf);
write(cfd, buf, len);
}
}
//干掉子进程
return 0;
}else if(pid>0){
//为了节省资源,关闭描述符
close(cfd);
}
}
close(lfd);
return 0;
}
上面代码会出现如下错误:
原因:
改进方法:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
//用于进程回收的回调函数
void recyle(int num){
pid_t pid;
while((pid=waitpid(-1, NULL, WNOHANG))>0){
printf("child died, pid=%d\n", pid);
}
}
int main(int argc, char *argv[]){
if(argc<2){
printf("eg: ./a.out port \n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len=sizeof(serv_addr);
int port=atoi(argv[1]);
//创建套接字
int lfd=socket(AF_INET, SOCK_STREAM, 0);
//初始化服务器
memset(&serv_addr, 0, serv_len);
//地址族
serv_addr.sin_family=AF_INET;
//监听本地所有IP
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//设置端口
serv_addr.sin_port=htons(port);
//绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
//设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept .... \n");
//使用信号回收子进程pcb
//不然让父进程一直用wait循环等待,那父进程就什么都做不了了
struct sigaction act;
act.sa_handler=recyle;
act.sa_flags=0;
//初始化mask,不然局部变量里面有什么我们不确定
sigemptyset(&act.sa_mask);
//规定我们要接受的信号
sigaction(SIGCHLD, &act, NULL);
struct sockaddr_in client_addr;
socklen_t cli_len=sizeof(client_addr);
while(1){
//父进程需要接受连接请求
//没有连接,那就阻塞着.如果阻塞被信号中断
//处理信号对应的操作之后,回来之后就不阻塞了,直接返回-1
//此时 errno=EINTR EINTR需要加头文件 errno.h
int cfd=accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
while((cfd==-1)&&(errno=EINTR)){
cfd=accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
}
printf("connect successful \n");
//创建子进程
pid_t pid=fork();
if(pid==0){
//关闭监听描述符
close(lfd);
char ip[64];
//子进程与客户端进程通信
while(1){
//当我接收到一条信息,我就打印对方的端口和ID
//inet_ntop,第二个参数是大端的整型IP,
//写在第三个参数指向的地址
//ntohs
printf("client IP: %s, port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,
ip, sizeof(ip)), ntohs(client_addr.sin_port));
char buf[1024];
int len=read(cfd, buf, sizeof(buf));
if(len==-1){
perror("read error");
exit(1);
}else if(len ==0){
printf("客户端断开了连接\n");
close(cfd);
break;
}else{
//把接受到的数据进行打印
printf("recv buf: %s\n", buf);
write(cfd, buf, len);
}
}
//干掉子进程
return 0;
}else if(pid>0){
//为了节省资源,关闭描述符
close(cfd);
}
}
close(lfd);
return 0;
}
TCP 多线程并发服务器
线程共享
- 全局数据
- 堆数据
- 一块有效内存的地址(我们此时要用这个)
多线程之后,一个cfd就不够用了,每个线程应该单独拥有一个cfd(文件描述符,用于通信的读写操作,lfd用于和客户端构建链接的文件描述符)
每个线程都需要一块自己独立的数据,因此每个独立的数据都要存到一块独立的内存里面。因此将线程需要的数据封装到结构体里面。
伪代码
typedef struct sockInfo {
pthread_t id;
int fd;
struct sockaddr_in addr;
}SockInfo;
void* worker(void* arg){
while(1){
// 打印客户端ip和port
read();
write();
}
}
int main(){
// 监听
int lfd = sock();
// 绑定
bind();
// 设置监听
listen();
SockInfo sock[256];
// 父线程
while(1){
sock[i].fd = accept(lfd, &&sock[i].addr, &len);
// 创建子线程
pthread_create(&sock[i].id, NULL, worker, &sock[i]);
pthread_deatch(sock[i].id);
}
}
多线程实现并发的代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>
// 自定义数据结构
typedef struct SockInfo {
int fd;
//存放IP地址的结构体
struct sockaddr_in addr;
pthread_t id;
}SockInfo;
// 子线程处理函数
void* worker(void* arg) {
char ip[64];
char buf[1024];
SockInfo* info = (SockInfo*)arg;
// 通信
while(1) {
printf("Client IP: %s, port: %d\n",
inet_ntop(AF_INET, &info->addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(info->addr.sin_port));
int len = read(info->fd, buf, sizeof(buf));
if(len == -1){
perror("read error");
pthread_exit(NULL);
}
else if(len == 0){
printf("客户端已经断开了连接\n");
close(info->fd);
break;
}
else {
printf("recv buf: %s\n", buf);
write(info->fd, buf, len);
}
}
return NULL;
}
int main(int argc, const char* argv[]){
if(argc < 2){
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
int i = 0;
//创建结构体数组
SockInfo info[256];
// 规定 fd == -1
for(i=0; i<sizeof(info)/sizeof(info[0]); ++i){
info[i].fd = -1;
}
socklen_t cli_len = sizeof(struct sockaddr_in);
while(1){
// 选一个没有被使用的, 最小的数组元素
for(i=0; i<256; ++i){
if(info[i].fd == -1){
break;
}
}
if(i == 256){
break;
}
// 主线程 - 等待接受连接请求
info[i].fd = accept(lfd, (struct sockaddr*)&info[i].addr, &cli_len);
//缺少一步对于fd的值是不是 -1 的判断
// 创建子线程 - 通信
pthread_create(&info[i].id, NULL, worker, &info[i]);
// 设置线程分离(这样,子线程死了就不需要我们去管了)
pthread_detach(info[i].id);
}
close(lfd);
// 只退出主线程,对子线程没有影响
pthread_exit(NULL);
return 0;
}