下面是我的另外一篇文章TCP协议和UDP协议的区别:
https://blog.csdn.net/qq_37941471/article/details/80888827
这样我们可以更清楚的理解程序
TCP服务器和客户端实现需要的接口:
下面介绍的接口都在这个头文件中:
#include <sys/socket.h>
#include <sys/types.h>
1. 创建套接字 ( socket() )
int socket(int domain, int type, int protocol);
int socket(int domain, int type, int protocol);
1.domain 指定使用何种的地址类型
协议 说明
PF_INET/AF_INET Ipv4 网络协议
PF_INET6/AF_INET6 Ipv6 网络协议
2. type的类别。见表:
类型 说明
SOCK_STREAM 提供双向连续且可信赖的数据流, 即TCP
SOCK_DGRAM 使用不连续不可信赖的数据包连接,即UDP
3. protocol 用来指定socket 所使用的传输协议编号,通常为0
4.返回值:成功(0);失败(-1)
5. socket()打开一个网络通讯接口,如果成功的话,就想open()一样返回一个文件描述符
2. 命名套接字
//2. 命名套接字
struct sockaddr_in local;
local.sin_family = AF_INET;//Ipv4: AF_INET;Ipv6: AF_INET6
local.sin_port = htons(atoi(argv[2]));//端口号是2位的数
local.sin_addr.s_addr = inet_addr(argv[1]);
3. 绑定端口号 ( bind() )
int bind(int sockfd, struct sockaddr * my_addr, int addrlen);
int bind(int sockfd, struct sockaddr * my_addr, int addrlen);
1. 返回值:成功(0);失败(-1)
2. 作用:将参数sockfd和myaddr绑定在一起
3. struct sockaddr * :是一个通用的结构体
4. 开始监听socket (TCP,服务器)
int listen(int socket,int backlog);
4. 接受请求 (TCP,服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
5. 建立连接 (TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addlen);
代码实现:
创建下面的文件:
Makefile Tcp_server.c Tcp_client.c
Makefile:
.PHONY:all clean
all:Tcp_server Tcp_client
Tcp_server:Tcp_server.c
gcc $^ -o $@
Tcp_client:Tcp_client.c
gcc $^ -o $@
clean:
rm -rf Tcp_serever Tcp_client
Tcp_server.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s ip port\n",argv[0]);
return 1;
}
//1. 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM , 0 );// SOCK_DGRAM 表示UDP
if(listen_sock < 0)
{
perror("socket");
return 2;
}
printf("Socker:%d\n",listen_sock);
//2. 命名套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));//端口号是2位的数
local.sin_addr.s_addr = inet_addr(argv[1]);
// 3. 绑定端口号
if( bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0 )
{
perror("bind");
return 3;
}
// 4. 开始监听listen_sock
if( listen(listen_sock,5) < 0 )//这里的5是允许有5个客户端连接等待,如果收到更多的请求则忽略
{
perror("listen");
return 4;
}
// 5. 接受请求
printf("bind and listen success,wait accept...\n");
for(;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if( new_sock < 0 )
{
perror("accept");
continue;//如果出现错误,则继续接受请求
}
printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
char buf[128];
while(1)
{
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if( s > 0 )
{
buf[s] = 0;
printf("client# %s \n",buf);
write(new_sock,buf,strlen(buf));
}
else if( s == 0 )
{
printf("quit!\n");
break;
}
else
{
perror(" read ");
break;
}
}
close(new_sock);// 如果不关闭,文件描述符泄漏
}
return 0;
}
Tcp_client.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s ip port\n",argv[0]);
return 1;
}
//1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM , 0 );// SOCK_DGRAM 表示UDP
if( sock < 0 )
{
perror("socket");
return 2;
}
printf("Socker:%d\n",sock);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
// 2. 建立连接
if( connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0 )
{
perror(" connnet ");
return 3;
}
char buf[128];
while(1)
{
printf("Please Enter:");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if( s > 0 )
{
buf[s-1] = 0;
write(sock,buf,strlen(buf));
read(sock,buf,sizeof(buf)-1);
printf("server echo# %s\n",buf);
}
}
return 0;
}
测试代码:
在建立连接之前应该把防火墙关掉:
sudo service iptables stop
1. 先编译运行服务器
2. 再编译运行客户端,并向服务器发送消息
3. 再连接一个客户端,会发现新连接的客户端在等待,等待别的客户端退出,自己才能连接上,这个时候服务器会收到客户端连接和退出以及新的连接的信息。
新连接的客户端处于阻塞状态:
退出之前的客户端连接:
新的连接重新建立连接:
这个时候服务器端同时也收到了两个客户端退出以及建立新的连接的消息:
另外,我们可以在同一网段和别的ip去建立连接:
一. 先去ping 别人的ip,下面就是成功的信息:
二. 再去向别人发送客户端的可执行文件:192.168.31.209 是对方的ip地址,:/home是把可执行文件发到对方的home目录下,最底下是输入对方的密码
三. 自己去运行服务器端:
192.168.31.32是我自己的ip地址,8080是绑定的端口
./Tcp_server 192.168.31.32 8080
四. 别人运行客户端:
ip地址和端口同服务器的
./Tcp_client 192.168.31.32 8080
我们可以发现上面的程序只能处理一个连接,有新的连接时会阻塞,所以为了改进这个程序,写出了多进程版本和多线程版本,以及两个版本之间的关系,及其优缺点:
多线程版本:
代码实现:
只需要将上面的服务器代码修改一下:
Tcp_server.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
void serviceIO (int sock)
{
char buf[128];
while(1)
{
ssize_t s = read(sock,buf,sizeof(buf)-1);
if( s > 0 )
{
buf[s] = 0;
printf("client# %s \n",buf);
write(sock,buf,strlen(buf));
}
else if( s == 0 )
{
printf("quit!\n");
break;
}
else
{
perror("read");
break;
}
}
close(sock);
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s ip port\n",argv[0]);
return 1;
}
//1. 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM , 0 );// SOCK_DGRAM 表示UDP
if(listen_sock < 0)
{
perror("socket");
return 2;
}
printf("Socker:%d\n",listen_sock);
//2. 命名套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));//端口号是2位的数
local.sin_addr.s_addr = htnol(INADDR_ANY);//INADDR_ANY可以绑定任意ip
// 3. 绑定端口号
if( bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0 )
{
perror("bind");
return 3;
}
// 4. 开始监听listen_sock
if( listen(listen_sock,5) < 0 )//这里的5是允许有5个客户端连接等待,如果收到更多的请求则忽略
{
perror("listen");
return 4;
}
// 5. 接受请求
printf("bind and listen success,wait accept...\n");
for(;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if( new_sock < 0 )
{
perror("accept");
continue;//如果出现错误,则继续接受请求
}
printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pid_t id = fork();
if( id == 0 )//child
{
close(listen_sock);
if( fork() > 0 )//这块创建了孙子进程,而fork()>0其实就是子进程,让其退出,这个时候父进程就可以回收子进程,继续去accpet,然后孙子进程实现服务
{
exit(0);
}
serviceIO(new_sock);
exit(0);
}
else//father
{
waitpid(id,NULL,0);
}
}
return 0;
}
测试代码:
1. 运行服务器代码,这里的ip地址是0代表可以为任意ip
2. 运行客户端代码,这里我是重新打开一个终端来实现的,所以服务器收到的连接信息是相同的ip,端口号不同,代表的是不同的进程
客户端一: 127.0.0.1:46845 这里的端口号是:46845
客户端一: 127.0.0.1:46846 这里的端口号是:46846
3. 服务器会一次收到连接和接受信息:
4. 客户端按照顺序全部退出,服务器会收到退出信息。
多线程版本:
代码实现:
只需要将上面的服务器代码修改一下:
Tcp_server.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/wait.h>
typedef struct Arg
{
int fd;
}Arg;
void ProcessRequest (int sock)
{
char buf[128];
while(1)
{
ssize_t s = read(sock,buf,sizeof(buf)-1);
if( s > 0 )
{
buf[s] = 0;
printf("client# %s \n",buf);
write(sock,buf,strlen(buf));
}
else if( s == 0 )
{
printf("quit!\n");
break;
}
else
{
perror("read");
break;
}
}
close(sock);
}
void* service(void* ptr)
{
Arg* arg = (Arg *)ptr;
ProcessRequest(arg->fd);
free(arg);
return NULL;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s ip port\n",argv[0]);
return 1;
}
//1. 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM , 0 );// SOCK_DGRAM 表示UDP
if(listen_sock < 0)
{
perror("socket");
return 2;
}
printf("Socker:%d\n",listen_sock);
//2. 命名套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));//端口号是2位的数
local.sin_addr.s_addr = htnol(INADDR_ANY);//INADDR_ANY可以绑定任意ip
// 3. 绑定端口号
if( bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0 )
{
perror("bind");
return 3;
}
// 4. 开始监听listen_sock
if( listen(listen_sock,5) < 0 )//这里的5是允许有5个客户端连接等待,如果收到更多的请求则忽略
{
perror("listen");
return 4;
}
// 5. 接受请求
printf("bind and listen success,wait accept...\n");
for(;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if( new_sock < 0 )
{
perror("accept");
continue;//如果出现错误,则继续接受请求
}
printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pthread_t id = 0;
Arg* arg = (Arg*)malloc(sizeof(Arg));
arg->fd = new_sock;
pthread_create(&id,NULL,service,(void *)arg);
pthread_detach(id);
}
return 0;
}
测试代码:
1. 运行服务器代码,这里的ip地址是0代表可以为任意ip
2. 运行客户端代码
3. 客户端按照顺序全部退出,服务器会收到退出信息。
多进程版本和多线程版本的优缺点:
多进程和多线程的共同优点:支持多连接
多进程的优点和缺点:
优点:
1.可以同时服务多个客户
2.易于编写
3.稳定。(任意一个进程挂掉,不会影响其他进程)
缺点:
1.效率低(创建子进程需要时间)
2.浪费资源
3.服务的客户有上限
4.性能较低
多线程的优点和缺点:
优点:
相对多进程版本而言,缓解了性能问题和资源问题。
缺点:
1. 不稳定。(任意一个线程挂掉,所有的线程都会退出)
2. 共享资源,有风险。
TCP协议通讯过程
1. 下面是基于TCP协议的客户端和服务器程序的一般流程:
TCP在进行建立连接和断开连接时是基于状态机的。
我们把在TCP传输连接的建立和释放中的通信双方主机的这些状态称之为“有限状态机”(Finite State Machine,FSM)。
tcp连接状态:
2. 服务器初始化 :
1. 调用socket : 创建文件描述符
2. 调用bind : 将当前的文件描述符的ip和port绑定起来(ip+port = 套接);如果这个端口已经被别人占用,则绑定(bind)失败
// 服务器有两个文件描述符
3. 调用listen : 声明当前这个文件描述符作为一个服务器的文件描述符。为后面的accpet做好准备
4. 调用accept : 阻塞,等待客户端请求连接;( 另外,accpet才是进行IO的 )
3. 客户端建立连接的过程 (三次握手):
1. 调用socket : 创建文件描述符
2. 调用connect :向服务器发起请求连接 (三次握手)
第一次: 客户端 :connect会发出SYN段 并阻塞 等待服务器的应答
第二次: 服务器端:收到客户端的SYN 会应答客户端一个SYN-ACK段(表示同意连接)
第三次: 客户端:收到SYN-ACK段后会从connect()返回 同时应答一个ACK段给服务器端
而在这个过程中,大家都会被问到一个问题就是:为什么是三次握手?而不是两次或者四次或者五次?
首先,tcp是可靠传输协议,需要三次握手建立连接服务。
三次握手的目的是“为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误”,这种情况是:client端发出了一个连接请求报文,而是因为某些未知的原因在某个网络节点上发生延迟、滞留,导致延迟到连接释放以后的某个时间才到达server端。本来这是一个早已失效的报文段,但是server收到此失效的报文之后,会误认为是client再次发出的一个新的连接请求,于是server端就向client又发出确认报文,表示同意建立连接。
如果不采用“三次握手”,那么只要server端发出确认报文就会认为新的连接已经建立了,但是client端此时并没有发出建立连接的请求,因此不会去向server端发送数据,server端没有收到数据就会一直等待,产生死锁现象,这样server端就会白白浪费掉很多资源。
如果采用“三次握手”的话就不会出现这种情况,client端首先发出连接请求并进入等待状态,server接收连接请求后同意建立连接,并向client返回报文段表示已经建立连接server进入SYN_RECV状态,client接收到server发出的确认信息后自己再发出确认信息,然后就可以建立直接通信。所以说只有三次握手在逻辑上才是最合适的,可以保障可靠性。
三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。
而这里的两次和四次的情况是类似的,五次和三次是类似的,明明可以三次握手达到效果为什么要去握手五次呢?所以有了三次握手。
4. 数据传输的过程:
在这里我们先理解一下 全双工 和 半双工 的概念:
全双工:在同一条连接中,同一时刻,通信的双方可以同时写数据
半双工:在同一条连接中,同一时刻,只能在一方来写数据
而这里建立连接后,TCP协议提供全双工的通信服务。
1. 服务器端 : accpet()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待
2. 客户端: 这个时候客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理。在此期间,客户端调用read()阻塞等待服务器的应答。
3. 服务器端:调用write()将处理结果发回给客户端(即客户端的read()等待服务器的响应),再次调用服务器端的read(),阻塞等待下一条请求。
4. 客户端:收到后从read()返回,发送下一条请求,如此循环下去进行通信
5. 断开连接的过程 (四次挥手):
正常情况下,如果客户端没有更多的请求了,就会调用close()关闭连接。下面就是断开连接(四次挥手)过程 :
第一次:(客户端) 会向服务器端发FIN段
第二次:此时(服务器端)收到FIN段,会回应一个ACK,同时read会返回0.
第三次:read返回以后,服务器端就知道客户端关闭了连接,也调用close()去关闭连接,这个时候服务器端向客户端发送FIN
第四次:客户端收到FIN后,再返回一个ACK给服务器
6. 如果四次挥手时,服务器先断开连接会出现什么情况?
两方建立连接,如果服务器端先断开连接,而客户端还开着;然后立即重启服务器端,会发现端口地址被占用,无法建立连接,因为主动断开连接的一方有一个等待的过程 ( TIME_WATE )。
服务器先断开连接后:
我们会发现,突然间断开服务器,而客户端还在连接中,立即重启服务器是不可以的!等一会才可以再次重启服务器。这是等一会的时间就是在四次挥手(断开连接)的时间。
解决TIME_WAIT状态失败引起的bind失败的方法:
在Tcp_server.c代码的socket( ) 和 bind( ) 调用之间插入如下代码:
int opt = 1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
int listen_sock = socket(AF_INET, SOCK_STREAM , 0 );// SOCK_DGRAM 表示UDP
if(listen_sock < 0)
{
perror("socket");
return 2;
}
printf("Socker:%d\n",listen_sock);
int opt = 1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//2. 命名套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));//端口号是2位的数
local.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY可以绑定任意ip
我们会发现,突然间断开服务器,而客户端还在连接中,立即重启服务器是可以的。