TCP通信
TCP
是面向连接的通信,所以在通信之前,客户端与服务器端必须通过三次握手建立连接,然后在通信完毕,还要通过四次挥手断开连接。
(一)相关函数
1.创建套接字
domain
:地址类型,
ipv4
、
ipv6
、
unix
的地址类型分别定义为常数
AF_INET
、
AF_INET6
、
AF_UNIX.
type
:socket传输类型,tcp通信是面向字节流的,所以为SOCK_STREAM
在网络通信时,我们的数据要从本主机通过网络发送到对端主机,数据在内存中存放的形式有大端或者小端两种形式,所以在向网络中传输数据是,网络就要按照一定的规定收发数据。
TCP/IP协议规定,网络字节流应按照大端字节流,即低地址高字节。
网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高低址,因为网络字节流为大端,也就是先发送数据的高位字节,在发送低位字节。
2.为了代码的可移植性,下面库函数为实现网络字节序列到主机字节序列的转换。
h = host主机
n = network网络
l = 长整形
s = 短整形
htonl代表主机字节序转换成网络字节序
3.将套接字与socket结构体绑定,socket结构体会指定ip,端口号,还有地址类型
一般服务器的端口号和ip是绑定的,众所周知的,而客户端的端口号可以随机分配的
IPV4 struct sockaddr_in 结构体的结构
IP地址+端口就称为socket,所以socket的结构体内包含有ip 和端口号
struct in_addr {
__be32 s_addr;
};
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
sin_family
:地址类型,
ipv4
、
ipv6
、
unix
的地址类型分别定义为常数
AF_INET
、
AF_INET6
、
AF_UNIX.
地址类型的作用
:因为在不同的环境下
socket
的数据结构不同,
ipv4
使用的
struct sockaddr_in
,而
ipv6
使用的是
struct sockaddr_in6
。但在网络编程里的许多函数需要传参
sockaddr
结构体,
Struct sockaddr
结构体类型就如同
void*
类型,它可以接受任意类型的结构体,所以在传参时就不需要知道具体的
socket
结构体类型,可以给根据地址类型来确定结构体的内容。
Sockeaddr
数据结构
4.将sockfd设置为监听套接字,并通过参数2是指明最多可以监听多少个套接字。
监听套接字的作用:server服务器启动后,会源源不断的有客户端来连接,这时候就需要一个监听套接字,来把一个个来访的socket按顺序存起来,并按顺序交给accept的去处理并返回newsockfd去收发数据,这样做可以保证在server满负荷的处理其他socket时,其他客户端要访问服务器时,可以通过监听队列等待一会,有其他客户端断开连接了,他就可以连接了。
5.
阻塞式等待客户端连接,监听套接字一直在监听是否有新连接到来连接,如果有链接则接受对方连接,连接之后由返回值new_sock收发数据
。
new_sock(1) = accept(监听套接字(2),struct socket_in输出型参数(3),输入输出型参数(4))
6.
client不需要被别人连接,只需要连接别人,所以使用connect来连接服务器
(二)TCP
通信的基本原理
三次握手建立连接:
server
端调用socket(),bind(),listen()创建监听套接字并完成初始化,然后调用accept()阻塞式等待客户连接。客户端创建一个套接字初始化后,调用connect连接server,连接过程:调用connect()发出SYN段并阻塞等待服务器应答(client:我想要连接你),服务器应答一个SYN-ACK(server:好,我准备好了,你连接吧),客户端收到从connect()返回,同时应答一个ACK给server(client:太好了,我连接好了),服务器收到ACK,从accept返回。
数据传输过程:建立连接后,TCP可以提供全双工的通信,server先读再写,client先写在读,用read()和write()阻塞式的等待一个写一个读。一直循环下去。
四次挥手关闭连接:假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!如果一方调用shutdown()则连接处于半关闭状态,仍可接受对方的数据。
(三)代码实现
server.c
clude
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#define _BACKLOG_ 10
int GetSocket(int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock<0){
perror("socket");
exit(1);
}
printf("%d:socket create is ok\n", sock);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY这个宏代表本地的任意ip地址,
//因为本地可能多个网卡。这个宏的值为0
if(bind(sock, (struct sockaddr*)&server,sizeof(server))<0){
perror("bind");
close(sock);
exit(2);
}
printf("bind is ok\n");
if(listen(sock, _BACKLOG_)< 0){
perror("listen");
close(sock);
return 3;
}
printf("listen is ok\n");
return sock;
}
void use(char *a)
{
printf("#%s [port_server]\n", a);
}
int main(int argc, char *argv[])
{
printf("main start\n");
if(argc<2)
{
use(argv[0]);
return 6;
}
printf("use is ok\n");
int listen_sock = GetSocket(atoi(argv[1]));
printf("GetSocket is ok\n");
struct sockaddr_in client;
socklen_t len = sizeof(client);
printf("wait accept....\n");
while(1)
{
int new_sock = accept(listen_sock,(struct sockaddr*)&client, &len);
if(new_sock< 0)
{
perror("accept");
close(new_sock);
return 4;
}
printf("[%s][%d]:accept is ok\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pid_t pid = fork();
if(pid < 0){
close(new_sock);
printf("process creation failed\n");
continue;
}else if(pid == 0){
close(listen_sock);
if(fork()>0){//fork()两次,使得孙子进程变成孤儿进程受init回收,父进程直接回收子进程,解决父进程阻塞问题
exit(2);
}
else{
while(1)
{
fflush(stdout);
char buf[1024];
ssize_t i = read(new_sock, buf, sizeof(buf));
if(i>0){
printf("[%s][%d]:client say#%s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
}else if(i == 0){
close(new_sock);
printf("[%s][%d]:client goodbye\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
break;
}else{
perror("read");
break;
}
printf("Enter to [%s][%d]#",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
fflush(stdout);
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';
write(new_sock, buf, strlen(buf));
}
}
}else{
close(new_sock);
waitpid(pid,NULL,0);
}
}
close(listen_sock);
return 0;
}
client.c
#include
#include
#include
#include
#include
#include
#include
#include
void use(char *argv)
{
printf("#%s [ip_server] [port_srvera]\n", argv);
}
int main(int argc, char *argv[])
{
printf("main start\n");
if(argc<3)
{
use(argv[0]);
return 3;
}
printf("use is ok\n");
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock<0){
perror("socket");
return 1;
}
printf("create socket is ok\n");
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]));
int conn = connect(sock, (struct sockaddr*)&server, sizeof(server));
if(conn<0){
perror("connect");
close(sock);
return 2;
}
while(1)
{
printf("please enter#");
fflush(stdout);
char buf[1024];
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';
write(sock, buf, sizeof(buf));
char* str = "quit";
if(strcmp(buf, str)==0){
break;
}
printf("server echo#");
fflush(stdout);
ssize_t r2 = read(sock, buf, sizeof(buf));
if(r2>0) {
printf("%s\n",buf);
}else{
continue;
}
}
close(sock);
printf("client goodbye!!!\n");
return 0;
}
(四)存在问题:
但是在运行的时候发现一个问题,server启动后,然后启动client建立连接后,然后直接ctrl+c终止掉server,无法立即重启,必须等待半分钟才能重新启动。如下图
这是因为在四次挥手断开连接时,主动断开的一方会进入TIME_WAIT状态,这是server还没有完全断开连接,还占着8080号端口,所以再次启动时创建监听套接字就无法在绑定上8080号端口。
但是这是不合理的,因为在实际生活中,服务器一旦挂了,不能立即重启,可能会影响许多客户的体验,会造成很大的损失,那么如何解决。
解决:
解决这个问题的方法是使用setsockopt()设置socket描述符的,设置选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。在server代码的socket()和bind()之间插入。