一、基础
cpp
复制代码
网络套接字:socket
一个文件描述符指向一个套接字。
套接字是成对出现的。(插头和插座)
网络字节序:
小端法:(PC本地存储) 高位存高地址。低位存低地址。
大端法:(网络存储)高位存低地址。低位存高地址。
// 二进制左边是高位
int a = 0x12345678
小端法:
地址0x00: 0x78
地址0x01: 0x56
地址0x02: 0x34
地址0x03: 0x12
大端法:
地址0x00: 0x12
地址0x01: 0x34
地址0x02: 0x56
地址0x03: 0x78
二、IP地址转换函数
cpp
复制代码
htonl: 本地 --> 网络(IP)
htons: 本地 --> 网络(port)
ntohl: 网络 --> 本地(IP)
ntohs: 网络 --> 本地(Port)
本地字节序(string IP)---> 网络字节序
int inet_pton(int af, const char *src, void *dst)
af:AF_FNET、AF_INET6
src:传入,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_INET6
src:网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值:
成功:dst。
失败:NULL
三、sockaddr结构
cpp
复制代码
struct sockaddr_in addr:
addr.sin_family = AF_INET/AF_INET6
addr.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函数
cpp
复制代码
创建一个套接字:
int socket(int domain, int type,int protocol);
domain: AF_INET、AF_INET6、AF_UNIX
type: SOCK_STREAI、SOCK_DGRAM
protocol: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 *)&addr
addrlen:sizeof(addr)地址结构的大小。
返回值:
成功:0
失败:-1errno
五、listen和accept函数
cpp
复制代码
设置同时与服务器建立连接的上限数。(同时进行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函数
cpp
复制代码
用现有的socket与服务器建立连接
int connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
sockfd: socket函数返回值
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值。
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构,采用"隐式绑定".
七、TCP实现客户端服务器连接
cpp
复制代码
// 服务器代码
#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 9527
void 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;
}
// 测试如下图
cpp
复制代码
// 客户端代码
#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 9527
void 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模型
九、封装错误处理代码
cpp
复制代码
将一系列自己写的函数和错误处理代码封装到另一个程序中
cpp
复制代码
// 服务器代码
#include"wrap.h"
#define SERV_PORT 9527
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); // 调用自己写的函数
ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if(ret != 0){
sys_err("bind error");
}
Listen(lfd, 128); // 调用自己写的函数
// 此处省略...
return 0;
}
cpp
复制代码
// 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
cpp
复制代码
// 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;
}
十、多进程服务器实现
cpp
复制代码
#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 9999
void 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;
}
// 这样实现了多个客户端访问一个服务器,同时不会产生僵尸进程
十一、多线程服务器实现
cpp
复制代码
#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 8000
struct 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 16
while (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,得到lfd
bzero(&servaddr, sizeof(servaddr)); // 地址结构清零
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定本地任意IP
servaddr.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连接图
cpp
复制代码
2MSL时长:保证最后一个ACK能成功被对端接收。
(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
十三、端口复用
cpp
复制代码
当服务器先关闭,再关闭客户端,再启动服务器就会出错
因为服务器主动关闭要等2MSL,期间端口还被占用
解决:在bind()之前插入以下代码
//listenfd = socket(...);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
十四、半关闭状态
cpp
复制代码
半关闭。
通信双方中,只有一端关闭通信。---FIN_WAIT_2
close(cfd);
shutdown(int fd, int how);
how:
SHUT_RD关读端
SHUT_WR关写端
SHUT_RDWR关读写
shutdown()在关闭多个文件描述符应用的文件时,采用全关闭方法。close()只关闭一个。
十五、多路IO
十六、select函数
cpp
复制代码
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,不在0
FD_ISSET(4, &rset);
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds:读文件描述符监听集合。 传入、传出参数
writefds:写文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常文件描述符监听集合 传入、传出参数 NULL
timeout:
>0:设置监听超时时长。
NULL:阻塞监听
0:非阻塞监听,轮询
返回值:
>0:所有监听集合(3个)中,满足对应事件的总数。
0:没有满足监听条件的文件描述符
-1:errno
十七、select实现多路IO
cpp
复制代码
#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 6666
int 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; //定义读集合,备份集合allset
int 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,后续执行无须执行
}
}
//处理满足读事件的fd
for (i = listenfd + 1; i <= maxfd; i++) {
//找到满足读事件的那个fd
if (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优缺点
cpp
复制代码
缺点:
监听上限受文件描述符限制。最大1024
轮询fd效率太慢,一个个问,有的可能用不到也被询
优点:
跨平台。Win、Linux、MacOS、Unix
解决:通过自定义数组放入需要被轮询的fd,避免不必要的轮询
cpp
复制代码
// 用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 6666
int 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;
}