一、基础
网络套接字:socket一个文件描述符指向一个套接字。套接字是成对出现的。(插头和插座)网络字节序:小端法:(PC本地存储) 高位存高地址。低位存低地址。大端法:(网络存储)高位存低地址。低位存高地址。// 二进制左边是高位
int a = 0x12345678小端法:
地址0x00: 0x78
地址0x01: 0x56
地址0x02: 0x34
地址0x03: 0x12大端法:
地址0x00: 0x12
地址0x01: 0x34
地址0x02: 0x56
地址0x03: 0x78
二、IP地址转换函数
htonl: 本地 --> 网络(IP)
htons: 本地 --> 网络(port)
ntohl: 网络 --> 本地(IP)
ntohs: 网络 --> 本地(Port)本地字节序(string IP)---> 网络字节序
int inet_pton(int af, const char *src, void *dst) af:AF_FNET、AF_INET6src:传入,IP地址(点分十进制)dst:传出,转换后的网络字节序的IP地址。
返回值:成功:1异常:0,说明src指向的不是一个有效的ip地址。失败:-1网络字节序 --> 本地字节序(string IP)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);af: AF_INET、AF_INET6src:网络字节序IP地址dst:本地字节序(string IP)size: dst 的大小。
返回值:成功:dst。失败:NULL
三、sockaddr结构
struct sockaddr_in addr:addr.sin_family = AF_INET/AF_INET6addr.sin_port = htons(9527);int dst;inet_pton(AF_INET, "192.157.22.45", (void *)&dst);addr.sin_addr.s_addr = dst;常用:addr.sin_addr.s_addr = htonl(INADDR_ANY) 取出系统中有效的任意IP地址。二进制类型。bind(fd, (struct sockaddr *)&addr, size):
四、socket和bind函数
创建一个套接字:
int socket(int domain, int type,int protocol);domain: AF_INET、AF_INET6、AF_UNIXtype: SOCK_STREAI、SOCK_DGRAMprotocol:0
返回值:成功:新套接字所对应文件描述符失败:-1 errno给socket绑定一个地址结构(IP+port):
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)sockfd:socket函数返回值struct sockaddr_in addr:addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = htonl(INADDR_ANY):addr: (struct sockaddr *)&addraddrlen:sizeof(addr)地址结构的大小。
返回值:成功:0失败:-1errno
五、listen和accept函数
设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
int listen(int sockfd, int backlog);sockfd:socket函数返回值backlog:上限数值。最大值128.
返回值:成功:0失败:-1 errno阻塞等待客户端建立连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);sockfd:socket函数返回值addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)socklen_t clit_addr_len = sizeof(addr):addrlen:传入传出。&clit_addr_len入:addr的大小。出:客户端addr实际大小。
返回值:成功:能与服务器进行数据通信的socket对应的文件描述。失败:-1,errno
六、connect函数
用现有的socket与服务器建立连接
int connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen);sockfd: socket函数返回值addr:传入参数。服务器的地址结构addrlen:服务器的地址结构的大小
返回值。成功:0失败:-1 errno
如果不使用bind绑定客户端地址结构,采用"隐式绑定".
七、TCP实现客户端服务器连接
// 服务器代码
#include<stdio.h>
#include<ctype.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>#define SERV_PORT 9527void sys_err(const char* str) {perror(str);exit(1);
}int main(int argc, char* argv[]) {int lfd = 0, cfd = 0;int ret, i;char buf[BUFSIZ], client_IP[1024];struct sockaddr_in serv_addr, clit_addr;socklen_t clit_addr_len;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1) {sys_err("socket error");}bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));listen(lfd, 128);clit_addr_len = sizeof(clit_addr);cfd = accept(lfd, (struct sockaddr*)&clit_addr, &clit_addr_len);if (cfd == -1) {sys_err("accept error");}printf("client ip:%s port:%d\n",inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),ntohs(clit_addr.sin_port));while (1) {ret = read(cfd, buf, sizeof(buf));write(STDOUT_FILENO, buf, ret);for (i = 0; i < ret; i++) {buf[i] = toupper(buf[i]);}write(cfd, buf, ret);}close(lfd);close(cfd);return 0;
}// 测试如下图

// 客户端代码
#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>#define SERV_PORT 9527void sys_err(const char* str) {perror(str);exit(1);
}int main(int argc, char* argv[]) {int cfd;int conter = 10;char buf[BUFSIZ];struct sockaddr_in serv_addr; //服务器地址结构serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);cfd = socket(AF_INET, SOCK_STREAM, 0);if (cfd == -1) {sys_err("socket error");}int ret = connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if (ret != 0) {sys_err("connect error");}while(--conter) {write(cfd, "hello\n", 6);ret = read(cfd, buf, sizeof(buf));write(STDOUT_FILENO, buf, ret);sleep(1);}close(cfd);return 0;
}

八、图解C-S模型

九、封装错误处理代码
将一系列自己写的函数和错误处理代码封装到另一个程序中
// 服务器代码
#include"wrap.h"#define SERV_PORT 9527int main(int argc, char* argv[]) {int lfd = 0, cfd = 0;int ret, i;char buf[BUFSIZ], client_IP[1024];struct sockaddr_in serv_addr, clit_addr;socklen_t clit_addr_len;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SERV_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);lfd = Socket(AF_INET, SOCK_STREAM, 0); // 调用自己写的函数ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));if(ret != 0){sys_err("bind error");}Listen(lfd, 128); // 调用自己写的函数// 此处省略...return 0;
}
// wrap.h
#ifndef _WRAP_H_
#define _WRAP_H_#include<stdio.h>
#include<ctype.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>void sys_err(const char* str);
int Socket(int domain, int type, int protocol);
int Listen(int sockfd, int backlog);#endif
// wrap.c
#include"wrap.h"void sys_err(const char* str) {perror(str);exit(1);
}int Socket(int domain, int type, int protocol) {int n = socket(domain, type, protocol);if (n = -1) {sys_err("socket error");return n;}return n;
}int Listen(int sockfd, int backlog) {int n = listen(sockfd, backlog);if (n = -1) {sys_err("listen error");return n;}return n;
}
十、多进程服务器实现
#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<errno.h>
#include<signal.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<pthread.h>// 封装的错误处理函数
#include"wrap.h"#define SRV_PORT 9999void catch_child(int signum) {while ((waitpid(0, NULL, WNOHANG)) > 0);return;
}int main(int argc, char* argv[]) {int lfd, cfd;pid_t pid;struct sockaddr_in srv_addr, clt_addr;socklen_t clt_addr_len;char buf[BUFSIZ];int ret, i;bzero(&srv_addr, sizeof(srv_addr)); // 将地址结构清零srv_addr.sin_family = AF_INET;srv_addr.sin_port = htons(SRV_PORT);srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);lfd = Socket(AF_INET, SOCK_STREAM, 0);Bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));Listen(lfd, 128);clt_addr_len = sizeof(clt_addr);while (1) {cfd = Accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);pid = fork();if (pid < 0) {sys_err("fork error");}else if (pid == 0) {close(lfd);break;}else {struct sigaction act;act.sa_handler = catch_child;sigemptyset(&act.sa_mask);act.sa_flags = 0;ret = sigaction(SIGCHLD, &act, NULL);if (ret != 0) {perr_exit("sigaction error");}close(cfd);continue;}}if (pid = 0) {while (1) {ret = Read(cfd, buf,sizeof(buf));if (ret = 0) {close(cfd);exit(1);}for (i = 0; i < ret; i++) {buf[i] = toupper(buf[i]);}write(cfd, buf, ret);write(STDOUT_FILENO, buf, ret);}}return 0;
}// 这样实现了多个客户端访问一个服务器,同时不会产生僵尸进程
十一、多线程服务器实现
#include<stdio.h>
#include<string.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<ctype.h>
#include<unistd.h>
#include<fcntl.h>#include"wrap.h"#define MAXLINE 8192
#define SERV_PORT 8000struct s_info { // 定义一个结构体,将地址结构跟cfd捆绑struct sockaddr_in cliaddr;int connfd;
};void* do_work(void* arg) {int n, i;struct s_info* ts = (struct s_info*)arg;char buf[MAXLINE];char str[INET_ADDRSTRLEN]; // #define INET_ADDRSTRLEN 16while (1) {n = Read(ts->connfd, buf, MAXLINE); // 读客户端if (n == 0) {printf("the client %d closed...\n", ts->connfd);break; // 跳出循环,关闭cfd}printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),ntohs((*ts).cliaddr.sin_port)); // 打印客户端信息(IP/PORT)for (i = 0; i < n; i++) {buf[i] = toupper(buf[i]); // 小写 --> 大写}Write(STDOUT_FILENO, buf, n); // 写出至屏幕Write(ts->connfd, buf, n); // 回写给客户端}Close(ts->connfd);/*Close函数如下:int n = close(fd);if(n == -1){sys_err("close error");}return n;*/ return (void*)0;
}int main(void) {struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int listenfd, connfd;pthread_t tid;struct s_info ts[256]; // 创建结构体数组.int i = 0;listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket,得到lfdbzero(&servaddr, sizeof(servaddr)); // 地址结构清零servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定本地任意IPservaddr.sin_port = htons(SERV_PORT); // 指定端口号Bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));Listen(listenfd, 128); // 设置同一时刻链接服务器上限数printf("Accepting client connect ...\n");while (1) {cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len); //阻塞监听客户端链接请求ts[i].cliaddr = cliaddr;ts[i].connfd = connfd;pthread_create(&tid, NULL, do_work, (void*)&ts[i]);pthread_detach(tid); //子线程分离,防止僵线程产生i++;}return 0;
}
十二、TCP连接图

2MSL时长:保证最后一个ACK能成功被对端接收。
(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
十三、端口复用
当服务器先关闭,再关闭客户端,再启动服务器就会出错
因为服务器主动关闭要等2MSL,期间端口还被占用解决:在bind()之前插入以下代码//listenfd = socket(...);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
十四、半关闭状态
半关闭。
通信双方中,只有一端关闭通信。---FIN_WAIT_2
close(cfd);shutdown(int fd, int how);
how:SHUT_RD关读端SHUT_WR关写端SHUT_RDWR关读写shutdown()在关闭多个文件描述符应用的文件时,采用全关闭方法。close()只关闭一个。
十五、多路IO


十六、select函数

void FD_ZERO(fd_set *set); ---清空一个文件描述符集合。fd_set rset;FD_ZERO(&rset);void FD_SET(int fd, fd_set *set); ---将待监听的文件描述符,添加到监听集合中。FD_SET(3, &rset);FD_SET(5, &rset);FD_SET(6, &rset);void FD_CLR(int fd, fd_set *set); ---将一个文件描述符从监听集合中移除。FD_CLR(4. &rset);int FD_ISSET(int fd, fd set *set); ---判断一个文件描述符是否在监听集合中。返回值:在1,不在0FD_ISSET(4, &rset);int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);nfds:监听的所有文件描述符中,最大文件描述符+1readfds:读文件描述符监听集合。 传入、传出参数 writefds:写文件描述符监听集合。 传入、传出参数 NULLexceptfds:异常文件描述符监听集合 传入、传出参数 NULLtimeout:>0:设置监听超时时长。NULL:阻塞监听0:非阻塞监听,轮询返回值:>0:所有监听集合(3个)中,满足对应事件的总数。0:没有满足监听条件的文件描述符-1:errno
十七、select实现多路IO

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<ctype.h>// 封装的错误处理函数
#include"wrap.h"#define SERV_PORT 6666int main(int argc, char* argv[]) {int listenfd, connfd;char buf[BUFSIZ];struct sockaddr_in clie_addr, serv_addr;socklen_t clie_addr_len;listenfd = Socket(AF_INET, SOCK_STREAM, 0);int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(SERV_PORT);Bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));Listen(listenfd, 128);fd_set rset, allset; //定义读集合,备份集合allsetint ret, maxfd = 0, n, i, j;maxfd = listenfd; //最大文件描述符FD_ZERO(&allset); //清空监听集合FD_SET(listenfd, &allset); //将待监听fd添加到监听集合中while (1) {rset = allset; //备份ret = select(maxfd + 1, &rset, NULL, NULL, NULL); //使用select监听if (ret < 0) {perr_exit("select error");}//listenfd满足监听的读事件if (FD_ISSET(listenfd, &rset)) { clie_addr_len = sizeof(clie_addr);//建立链接,不会阻塞connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len);FD_SET(connfd, &allset); //将新产生的fd,添加到监听集合中,监听数据读事件if (maxfd < connfd) {maxfd = connfd; //修改maxfd}if (ret == 1) {continue; //说明select只返回一个,并且是listenfd,后续执行无须执行}}//处理满足读事件的fdfor (i = listenfd + 1; i <= maxfd; i++) { //找到满足读事件的那个fdif (FD_ISSET(i, &rset)) { n = Read(i, buf, sizeof(buf));//检测到客户端已经关闭链接if (n == 0) { Close(i);FD_CLR(i, &allset); //将关闭的fd,移除出监听集合}else if (n == -1) {perr_exit("read error");}for (j = 0; j < n; j++) {buf[j] = toupper(buf[j]);}write(i, buf, n);write(STDOUT_FILENO, buf, n);}}}Close(listenfd);return 0;
}
十八、select优缺点
缺点:监听上限受文件描述符限制。最大1024轮询fd效率太慢,一个个问,有的可能用不到也被询优点:跨平台。Win、Linux、MacOS、Unix解决:通过自定义数组放入需要被轮询的fd,避免不必要的轮询
// 用client数组解决
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<ctype.h>#include"wrap.h"#define SERV_PORT 6666int main(int argc, char* argv[]) {int i, j, n, maxi;/*自定义数组client,防止遍历1024个文件描述符 FD_SETSIZE默认为1024*/int nready, client[FD_SETSIZE];int maxfd, listenfd, connfd, sockfd;/* #define INET_ADDRSTRLEN 16 */char buf[BUFSIZ], str[INET_ADDRSTRLEN];struct sockaddr_in clie_addr, serv_addr;socklen_t clie_addr_len;fd_set rset, allset; /*set读事件文件描述符集合allset用来暂存*/listenfd = Socket(AF_INET, SOCK_STREAM, 0);int opt = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(SERV_PORT);Bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));Listen(listenfd, 128);maxfd = listenfd; /*起初listenfd即为最大文件描述符*/maxi = -1; /*将来用作client[]的下标,初始值指向0个元素之前下标位置*/for (i = 0; i < FD_SETSIZE; i++) {client[i] = -1; /*用-1初始化client[]*/}FD_ZERO(&allset);FD_SET(listenfd, &allset); /*构造select监控文件描述符集*/while (1) {rset = allset; /*每次循环时都重新设置select监控信号集*/nready = select(maxfd + 1, &rset, NULL, NULL, NULL);if (nready < 0) {perr_exit("select error");}/*说明有新的客户端连接请求*/if (FD_ISSET(listenfd, &rset)) {clie_addr_len = sizeof(clie_addr);//建立连接,不会阻塞connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len);printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),ntohs(clie_addr.sin_port));for (i = 0; i < FD_SETSIZE; i++) {/*找client[]中没有使用的位置*/if (client[i] < 0) {client[i] = connfd; /*保存accept返回的文件描述符到client[]里*/break;}}/*达到select能监控的文件个数上限1024*/if (i == FD_SETSIZE) {fputs("too many clients\n", stderr);exit(1);}FD_SET(connfd, &allset); /*向监控文件描述符集合allset添加新的文件描述符connfd*/if (connfd > maxfd) {maxfd = connfd; /*select第一个参数需要*/}if (i > maxi) {maxi = i; /*保证maxi存的总是client[]最后一个元素下标*/}if (--nready == 0) {continue;}}/*检测哪个clients有数据就绪*/for (i = 0; i <= maxi; i++) {if ((sockfd = client[i]) < 0) {continue;}if (FD_ISSET(sockfd, &rset)) {/*当client关闭连接时,服务器端也关闭对应连接*/if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {Close(sockfd);FD_CLR(sockfd, &allset); /*解除select对此文件描述符的监控*/client[i] = -1;}else if (n > 0) {for (j = 0; j < n; j++) {buf[j] = toupper(buf[j]);}Write(sockfd, buf, n);Write(STDOUT_FILENO, buf, n);if (--nready == 0) {break;}}}}}Close(listenfd);return 0;
}