一、socket地址
二、IP地址转换函数
/*
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
*/
#include<arpa/inet.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
char buf[] = "192.168.1.14";
unsigned int num;
inet_pton(AF_INET,buf,&num);//将点分十进制的ip字符串转换成网络字节序的整数
//结果保存到num里面了
//将num里面的东西一个一个字节打印出来看看
//转换为char型,
unsigned char *p = (unsigned char*)#
printf("%d ,%d ,%d ,%d\n",*p,*(p+1),*(p+2),*(p+3));
//printf("p : %s\n",p);
printf("====================\n");
//将网络字节序的IP整数转换成点分十进制的IP字符串
char ip[16] = "";
const char *str = inet_ntop(AF_INET,&num,ip,sizeof(ip));
//printf("%d ,%d ,%d ,%d\n",*p,*(p+1),*(p+2),*(p+3));
printf("str : %s\n",str);
return 0;
}
三、TCP通信流程
四、socket函数
五、TCP通信实现(服务器端)
// TCP通信的服务器端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
int main(){
//1、创建socket(用于监听) 成功返回文件描述符,失败返回-1
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd == -1){
perror("socket");
return -1;
}
//2、绑定ip 和端口 成功返回0失败返回-1
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
//192.168.78.161是字符串,而saddr.sin_addr.s_addr传入的值只能为整数,所以要使用inet_pton将字符串形式的ip地址转换为整数型的网络字节序
//unsigned int *buf;
//inet_pton(AF_INET,"192.168.78.161",&buf);
//saddr.sin_addr.s_addr = &buf;
//服务端的偷懒写法,INADDR_ANY就是0,写0代表任意地址,表示任意ip都能访问到我们
saddr.sin_addr.s_addr = INADDR_ANY;
//将主机端口号为9999的转换为网络字节序 主机字节序->网络字节序
unsigned short sprot = htons(9999);
saddr.sin_port = sprot;
// saddr的类型是sockaddr_in,而bind里面要传入sockaddr类型的,所以要强制转换一下
int ret = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
return -1;
}
// 3、监听 成功返回0失败返回-1
ret = listen(sockfd,8);
if(ret == -1){
perror("listen");
return -1;
}
// 4、接收客户端连接 成功返回文件描述符,失败返回-1 accept里面的第三个参数长度,该数据类型是指针,和bind不一样
// 阻塞函数,如果没有客户端连接,会一直阻塞在这
//clientaddr 是已经连接进来的客户端的信息
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int accfd = accept(sockfd,(struct sockaddr *)&clientaddr,&len);
if(accfd == -1){
perror("accept");
return -1;
}
//程序走到这里,说明已经保存了客户端信息,我们打印出来看看
//上面是网络字节序,打印的时候要转换成主机字节序
//xxx.xxx.xxx.xxx 3*4+3=15最后还有一个\0 一共16个字节
char clientIP[16];
inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,clientIP,sizeof(clientIP));
//不要忘记网络字节序转主机字节序
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s , port is %d \n",clientIP,clientPort);
//5、通信
//获取客户端的数据
char recvBuf[1024] = {0};
while(1){
int lens = read(accfd,recvBuf,sizeof(recvBuf));
if(lens == -1){
perror("read");
return -1;
}else if (lens >0){
//读到了数据
//将读到的数据输出出来
printf("recv client data : %s \n",recvBuf);
}else if(lens == 0){
//表示客户端断开连接
printf("客户端端口连接。。。。");
break;
}
//给客户端发送数据
char *data = "hello, i am server";
write(accfd,data,strlen(data));
//其实服务端可以不用sleep,因为客户端睡眠1秒才发送,这1秒内服务器端处于阻塞状态,没有数据可读
//sleep(1);
}
//关闭文件描述符
close(accfd);
close(sockfd);
return 0;
}
六、TCP通信实现(客户端)
//TCP通信 客户端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
int main(){
//1、创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//2、连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.78.161",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
if(ret == -1){
perror("connect");
return -1;
}
//3、通信
char recvBuf[1024]={0};
while(1){
char *data = "hello , i am client";
//给服务器端发送数据
write(sockfd,data,strlen(data));
sleep(1);
//读数据
int len = read(sockfd,recvBuf,sizeof(recvBuf));
if(len == -1){
perror("read");
return -1;
}else if(len > 0){
//读到了数据
printf("recv server data : %s\n",recvBuf);
}else if(len == 0){
//服务器端断开连接
printf("server closed.....");
break;
}
}
//关闭连接
close(sockfd);
return 0;
}
七、 TCP三次握手
八、 滑动窗口
窗口理解为缓冲区的大小
滑动窗口的大小会随着发送数据和接收数据而变化
通信双方都有发送数据的缓冲区和接收数据的缓冲区
服务器:
发送缓冲区 (发送缓冲区的窗口)
接收缓冲区 (接收缓冲区的窗口)
客户端:
发送缓冲区 (发送缓冲区的窗口)
接收缓冲区 (接收缓冲区的窗口)
发送方的缓冲区:
白色格子:空闲的空间
灰色格子:数据已经被发送出去了,但是还没有被接收
紫色格子:还没有发送出去的数据
接收方的缓冲区:
白色格子:空闲的空间
紫色格子:已经接收到的数据
mss : Maximum Segment Size(一条数据的最大的数据量)
win : 滑动窗口
1、客户端向服务器发起连接,客户端的滑动窗口是4096,一次发送的最大数据量是1460
2、服务器接收连接请求,告诉客户端服务器窗口大小是6144,一次发送的最大数据量是1024
3、第三次握手
4、4-9客户端连续给服务器发送了6k的数据,每次发送1k
5、第10次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
6、第11次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
7、第12次,客户端给服务器发送了1k的数据
剩下的就是四次挥手的内容
8、第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
9、第14次,服务器回复ACK 8194, a:同意断开连接的请求 b:告诉客户端已经接收到刚才发的2k的数据 c:滑动窗口2k
10、第15、16次,通知客户端滑动窗口的大小
11、第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12、第18次,第四次挥手,客户端同意了服务器端的断开请求
九、 TCP四次握手
客户端和服务端建立连接的时候,需要三次握手操作,来确保通信双方都是正常的。
tcp四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手
十、 TCP通信并发(多进程)
要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决
思路:
1、一个父进程,多个子进程
2、父进程负责等待并接受客户端的连接
3、子进程:完成通信,接收一个客户端连接,就创建一个子进程用于通信。
server_process.c
//TCP通信 客户端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
int main(){
//1、创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//2、连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.78.165",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
if(ret == -1){
perror("connect");
return -1;
}
//3、通信
char recvBuf[1024]={0};
int i = 0;
while(1){
//char *data = "hello , i am client";
sprintf(recvBuf,"data : %d \n",i++);
//给服务器端发送数据
//strlen +1 是为了把结束符 \0 加进去,不然会出错
write(sockfd,recvBuf,strlen(recvBuf)+1);
//读数据
int len = read(sockfd,recvBuf,sizeof(recvBuf));
if(len == -1){
perror("read");
return -1;
}else if(len > 0){
//读到了数据
printf("recv server data : %s\n",recvBuf);
}else if(len == 0){
//服务器端断开连接
printf("server closed.....");
break;
}
sleep(1);
}
//关闭连接
close(sockfd);
return 0;
}
client_process.c
//TCP通信 客户端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
int main(){
//1、创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//2、连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.78.165",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
if(ret == -1){
perror("connect");
return -1;
}
//3、通信
char recvBuf[1024]={0};
int i = 0;
while(1){
//char *data = "hello , i am client";
sprintf(recvBuf,"data : %d \n",i++);
//给服务器端发送数据
//strlen +1 是为了把结束符 \0 加进去,不然会出错
write(sockfd,recvBuf,strlen(recvBuf)+1);
//读数据
int len = read(sockfd,recvBuf,sizeof(recvBuf));
if(len == -1){
perror("read");
return -1;
}else if(len > 0){
//读到了数据
printf("recv server data : %s\n",recvBuf);
}else if(len == 0){
//服务器端断开连接
printf("server closed.....");
break;
}
sleep(1);
}
//关闭连接
close(sockfd);
return 0;
}
服务器端效果图
十一、TCP通信并发(多线程)
#include<stdio.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
#include<wait.h>
#include<errno.h>
#include<pthread.h>
struct sockInfo{
int fd; //通信的文件描述符
pthread_t tid; //线程号
struct sockaddr_in addr;
};
struct sockInfo sockinfos[128];
void *working(void * arg){
//子线程和客户端通信 cfd,客户端的信息,线程号
//获取客户端的信息
struct sockInfo *pinfo = (struct sockInfo *)arg;
//定义一个数组保存客户端的信息&accaddr.sin_addr.s_addr
char cliIp[16];
inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,cliIp,sizeof(cliIp));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, port is : %d\n",cliIp,cliPort);
//接收客户端发来的信息
char resvBuf[1024];
while(1){
int readlens = read(pinfo->fd,&resvBuf,sizeof(resvBuf));
if(readlens == -1){
perror("read");
//return -1;
exit(-1);
}else if(readlens == 0){
//客户端断开了连接
printf("client closed.....\n");
break;
}else if(readlens > 0){
printf("resv client : %s\n",resvBuf);
}
//把读出来的数据再写回去,回射服务器
write(pinfo->fd,resvBuf,strlen(resvBuf)+1);
}
close(pinfo->fd);
return NULL;//退出当前子进程
}
int main(){
//创建socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//绑定ip和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
//socklen_t slen = sizeof(saddr);
int ret = bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
return -1;
}
//监听
ret = listen(sockfd,128);
if(ret == -1){
perror("listen");
return -1;
}
//初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i<max;i++){
bzero(&sockinfos[i],sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
//循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
//接受连接
int cfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len);
struct sockInfo *pinfo;
for(int i =0;i<max;++i){
//从这个数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1){
pinfo = &sockinfos[i];
break;
}
if( i == max -1){
sleep(1);
i--;
}
}
pinfo -> fd = cfd;
memcpy(&pinfo->addr,&cliaddr,len);
//创建子线程
//pthread_t tid;
pthread_create(&pinfo->tid,NULL,working,pinfo);
//回收子线程资源
//不能使用pthread_join 因为join是阻塞的,其他客户端进不来
pthread_detach(pinfo->tid);
}
close(sockfd);
return 0;
}
client和前面一样
十二、TCP状态转换
十三、半关闭、端口复用
半关闭在上面
setsockopt不仅仅能设置端口复用,还能设置套接字的属性
参数:
sockfd:要操作的文件描述符
level:级别 - SOL_SOCKET(端口复用的级别)
optname:选项的名称
SO_REUSEADDR
SO_REUSEPORT
optval:端口复用的值(整形)
1:可以复用
0:不可以复用
optlen:optval参数的大小
端口复用,设置的时机是在服务器绑定端口之前
setsockopt()
bind()
查看网络相关信息的命令
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,而不通过域名服务器
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
//int optval = 1;
//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
return -1;
}
// 监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
return -1;
}
// 接收客户端连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1) {
perror("accpet");
return -1;
}
// 获取客户端信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
// 输出客户端的信息
printf("client's ip is %s, and port is %d\n", cliIp, cliPort );
// 接收客户端发来的数据
char recvBuf[1024] = {0};
while(1) {
int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
if(len == -1) {
perror("recv");
return -1;
} else if(len == 0) {
printf("客户端已经断开连接...\n");
break;
} else if(len > 0) {
printf("read buf = %s\n", recvBuf);
}
// 小写转大写
for(int i = 0; i < len; ++i) {
recvBuf[i] = toupper(recvBuf[i]);
}
printf("after buf = %s\n", recvBuf);
// 大写字符串发给客户端
ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
if(ret == -1) {
perror("send");
return -1;
}
}
close(cfd);
close(lfd);
return 0;
}