简单的TCP 客户端 - 服务器模型 run_client
函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 void run_client () { int client = socket (PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = inet_addr ("127.0.0.1" ); servaddr.sin_port = htons (9527 ); int ret = connect (client, (struct sockaddr*)&servaddr, sizeof (servaddr)); if (ret == 0 ) { printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); char buffer[256 ] = "hello,hello\n" ; write (client, buffer, sizeof (buffer)); memset (buffer, 0 , sizeof (buffer)); read (client, buffer, sizeof (buffer)); std::cout << buffer; } else { printf ("%s(%d):%s %d\n" , __FILE__, __LINE__, __FUNCTION__, ret); } close (client); std::cout << "client done!!" << std::endl; }
run_server()
函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 void lession63_ () { int server, client; struct sockaddr_in seraddr, clientaddr; socklen_t cliaddrlen; server = socket (PF_INET, SOCK_STREAM, 0 ); if (server < 0 ) { std::cout << "error!!\n" ; return ; } memset (&seraddr, 0 , sizeof (seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_addr.s_addr = inet_addr ("0.0.0.0" ); seraddr.sin_port = htons (9527 ); int ret = bind (server, (struct sockaddr*)&seraddr, sizeof (seraddr)); if (ret == -1 ) { std::cout << "bind failed!" << std::endl; close (server); return ; } ret = listen (server, 3 ); if (ret == -1 ) { std::cout << "listen failed!" << std::endl; close (server); return ; } char buffer[1024 ]; while (1 ) { memset (buffer, 0 , sizeof (buffer)); printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); client = accept (server, (struct sockaddr*)&clientaddr, &cliaddrlen); if (client == -1 ) { std::cout << "client failed!" << std::endl; close (server); return ; } printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); read (client, buffer, sizeof (buffer)); ssize_t len = write (client, buffer, strlen (buffer)); if (len!= (ssize_t )strlen (buffer)) { std::cout << "write failed!" << std::endl; close (server); return ; } printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); close (client); } close (server); printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); }
lession63()
函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void lession63 () { pid_t pid = fork(); if (pid == 0 ) { sleep (1 ); run_client (); run_client (); } else if (pid > 0 ) { lession63_ (); int status = 0 ; wait (&status); } else { std::cout << "fork failed!" << pid << std::endl; } }
总结 TCP连接客户端的流程
1️⃣创建套接字
2️⃣初始化服务器地址
3️⃣连接服务器。connect 成功返回0,失败返回-1
4️⃣通信 write read
5️⃣关闭套接字 (close关闭)
TCP连接服务器的流程
1️⃣创建套接字
2️⃣初始化服务器地址
3️⃣绑定套接字,bind
函数将服务器套接字 server
绑定到指定的地址和端口。
4️⃣监听连接,listen
函数使得服务器套接字开始监听传入的连接请求
5️⃣accept
接受并处理客户端连接,返回一个新的套接字描述符用于与客户端通信。 read
、write
进行通信。
6️⃣关闭服务器套接字,close(server)
回声服务器 回声服务器 :将从客户端收到的数据原样返回给客户端,即“回声”。
客户端部分 分为 发送 和 接受两部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 void run_client64 () { int client = socket (PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in servaddr; memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = inet_addr ("127.0.0.1" ); servaddr.sin_port = htons (9527 ); int ret = connect (client, (struct sockaddr*)&servaddr, sizeof (servaddr)); while (ret == 0 ) { printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); char buffer[256 ] = "" ; fputs ("Input message(Q to quit):" , stdout); fgets (buffer, sizeof (buffer), stdin); if ((strcmp (buffer, "q\n" ) == 0 || strcmp (buffer, "Q\n" ) == 0 )) { break ; } size_t len = strlen (buffer); size_t send_len = 0 ; while (send_len < len) { ssize_t ret = write (client, buffer + send_len, len -send_len); if (ret <= 0 ) { fputs ("write failed!\n" , stdout); close (client); std::cout << "client done!!" << std::endl; return ; } send_len += (size_t )ret; } memset (buffer, 0 , sizeof (buffer)); size_t read_len = 0 ; while (read_len < len) { ssize_t ret = read (client, buffer + read_len, len - read_len); if (ret <= 0 ) { fputs ("read failed!\n" , stdout); close (client); std::cout << "client done!!" << std::endl; return ; } read_len += (size_t )ret; } std::cout << "from server " << buffer; } close (client); std::cout << "client done!!" << std::endl; }
服务器端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 void server64 () { int server, client; struct sockaddr_in seraddr, clientaddr; socklen_t cliaddrlen; server = socket (PF_INET, SOCK_STREAM, 0 ); if (server < 0 ) { std::cout << "create socket failed!!\n" ; return ; } memset (&seraddr, 0 , sizeof (seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_addr.s_addr = inet_addr ("0.0.0.0" ); seraddr.sin_port = htons (9527 ); int ret = bind (server, (struct sockaddr*)&seraddr, sizeof (seraddr)); if (ret == -1 ) { std::cout << "bind failed!" << std::endl; close (server); return ; } ret = listen (server, 3 ); if (ret == -1 ) { std::cout << "listen failed!" << std::endl; close (server); return ; } char buffer[1024 ]; for (int i = 0 ; i < 2 ; i ++ ) { memset (buffer, 0 , sizeof (buffer)); printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); client = accept (server, (struct sockaddr*)&clientaddr, &cliaddrlen); if (client == -1 ) { std::cout << "client failed!" << std::endl; close (server); return ; } printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); ssize_t len = 0 ; while (( len = read (client, buffer, sizeof (buffer)) ) > 0 ) { len = write (client, buffer, len); if (len != (ssize_t )strlen (buffer)) { std::cout << "write failed! len: " << len << "buffer: " << buffer << std::endl; close (server); return ; } memset (buffer, 0 , len); } if (len <= 0 ) { std::cout << "read failed!" << std::endl; close (server); return ; } printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); close (client); } close (server); printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); }
主函数:启动服务器和客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void lession64 () { pid_t pid = fork(); if (pid == 0 ) { server64 (); } else if (pid > 0 ) { for (int i = 0 ; i < 2 ; i ++) run_client64 (); int status = 0 ; wait (&status); } else { std::cout << "fork failed!" << pid << std::endl; } }
问题汇总 客户端设置服务器地址
客户端设置服务器地址是为了明确要连接的目标服务器位置。它通过struct sockaddr_in
结构体来存储服务器的网络地址信息,包括 IP
地址和端口号。
服务器端设置服务器地址
服务器端设置自己的地址主要是为了将服务器套接字绑定到一个特定的网络接口和端口上,以便接收来自客户端的连接请求。
connect()
函数 和 bind()
函数 的区别
connect
函数主要用于客户端套接字与服务器端套接字建立连接。它尝试将客户端的网络端点(由 IP 地址和端口号标识)与服务器的网络端点进行关联,从而建立起一条通信链路,使得客户端和服务器可以互相发送和接收数据。
返回0:客户端与服务器之间的连接已经建立,可以开始进行数据读写操作了。
返回-1:执行 失败的时候会返回-1,errno变量来指示具体错误原因。
bind
函数主要用于服务器端将套接字绑定到一个特定的本地网络端点(包括 IP 地址和端口号)。它的作用是让服务器在特定的网络地址和端口上监听客户端的连接请求,使得客户端能够准确地找到服务器并与之建立连接。
write
函数
fd
:文件描述符,它可以代表一个打开的文件、套接字等。在网络编程中,通常是通过 socket
函数创建并连接好的套接字描述符。
buf
:指向要写入数据的缓冲区的指针。
count
:要写入的字节数。
返回值大于0:当 write
函数成功写入部分或全部数据时,它会返回实际写入的字节数。这个返回值可能小于、等于 count
参数指定的字节数。
**等于 count
**:表示成功写入了请求的所有字节。例如,你希望写入 10 个字节的数据,write
函数返回 10,说明这 10 个字节都成功写入到了文件描述符所关联的目标中。
**小于 count
**:可能是因为某些原因(如磁盘空间不足、网络缓冲区已满等),无法一次性写入所有请求的字节。在这种情况下,你可能需要再次调用 write
函数,从上次写入结束的位置继续写入剩余的数据。
返回值等于0:通常出现了一些异常情况
返回值为-1 :write
函数执行失败时,它会返回-1,并且会设置errno
变量来指示具体的错误原因。
这俩部分设置是否一样?
客户端角度
客户端设置的服务器 IP 地址必须是服务器实际绑定并监听的 IP 地址之一。如果服务器绑定了特定的 IP 地址(例如192.168.1.100
),客户端就需要使用这个 IP 地址来连接服务器。不过,如果服务器绑定的是0.0.0.0
(表示监听所有本地网络接口),客户端可以使用服务器所在主机的任何一个有效本地 IP 地址来连接。
服务器角度
服务器绑定的 IP 地址决定了它在哪些网络接口上监听客户端连接。如果服务器有多个网络接口(例如有多个网卡,每个网卡有不同的 IP 地址),绑定0.0.0.0
意味着在所有接口上监听;而绑定特定的 IP 地址则只在该接口监听。
主机字节序和网络字节序
大端字节序也成为网络字节序,数据的高位字节存于低地址,低位字节存于高地址。
小端字节序:数据低位字节存于低地址,高位字节存于高地址。
简单的 UDP
客户端 - 服务器模型 server
()函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 int server (int argc, char * argv[]) { int ser_sock = -1 ; char message[512 ]; struct sockaddr_in servaddr, clientaddr; socklen_t clientlen = 0 ; if (argc != 2 ) { printf ("usage:%s <port>\n" , argv[0 ]); handle_error ("argement is error!:" ); } ser_sock = socket (PF_INET, SOCK_DGRAM, 0 ); if (ser_sock == -1 ) { handle_error ("create socket failed:" ); } memset (&servaddr, 0 , sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons ((short )atoi (argv[1 ])); if (bind (ser_sock, (struct sockaddr*)&servaddr, sizeof (servaddr)) == -1 ) { handle_error ("bind failed:" ); } for (int i = 0 ; i < 10 ; i++) { clientlen = sizeof (clientaddr); ssize_t len = recvfrom (ser_sock, message, sizeof (message), 0 , (struct sockaddr*)&clientaddr, &clientlen); sendto (ser_sock, message, len, 0 , (struct sockaddr*)&clientaddr, clientlen); } close (ser_sock); return 0 ; }
client()
函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 int client (int argc, char * argv[]) { int client_sock; struct sockaddr_in serv_addr; socklen_t serv_len = sizeof (serv_addr); char message[512 ] = "" ; if (argc != 3 ) { printf ("usage:%s is port\n" , argv[0 ]); handle_error ("argement error:" ); } client_sock = socket (PF_INET, SOCK_DGRAM, 0 ); if (client_sock == -1 ) { handle_error ("socket create failed!" ); } memset (&serv_addr, 0 , sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr (argv[1 ]); serv_addr.sin_port = htons ((short )atoi (argv[2 ])); while (1 ) { printf ("Input message(q to quit):" ); scanf ("%s" , message); if ((strcmp (message, "q" ) == 0 ) || (strcmp (message, "Q" ) == 0 )) { printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); break ; } printf ("%s(%d):%s\n" , __FILE__, __LINE__, __FUNCTION__); ssize_t len = sendto (client_sock, message, strlen (message), 0 , (sockaddr*)&serv_addr, serv_len); memset (message, 0 , len); recvfrom (client_sock, message, sizeof (message), 0 , (sockaddr*)&serv_addr, &serv_len); printf ("recv:%s\n" , message); } close (client_sock); return 0 ; }
总结 UDP
客户端的流程
1️⃣ 创建套接字
2️⃣ 设置服务器地址端口
3️⃣ 使用sendto
函数发送数据、recvfrom
函数来接收数据。
4️⃣ 关闭套接字
UDP
服务器端的流程
1️⃣ 创建套接字
2️⃣ 绑定地址和端口
3️⃣ 接受、处理数据
4️⃣ 关闭套接字
问题汇总 UDP
服务器端/客户端的实现方法。但如果仔细观察UDP
客户端会发现,它缺少把IP
和端口分配给套接字的过程。
TCP
客户端调用connect函数自动完成此过程,UDP
调用sendto
函数时自动分配IP
和端口号
套接字类型只能在创建时决定,以后不能再更改。
SO_SNDBUF &SO_RCVBUF
SO_RCVBUF
是输入缓冲大小相关可选项,SO_SNDBUF
是输出缓冲大小相关可选项。用这2 个可选项既可以读取当前I/O缓冲大小,也可以进行更改。通过下列示例读取创建套接字时默认的I/O缓冲大小。
如何使用socket
、getsockopt
和setsockopt
函数来获取和设置套接字的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void lession76 () { int tcp_sock, udp_sock; int optval = 0 ; socklen_t len = sizeof (optval); tcp_sock = socket (PF_INET, SOCK_STREAM, 0 ); udp_sock = socket (PF_INET, SOCK_DGRAM, 0 ); printf ("SOCK_STREAM:%d\n" , SOCK_STREAM); printf ("SOCK_DGRAM:%d\n" , SOCK_DGRAM); getsockopt (tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&optval, &len); printf ("tcp_sock type is :%d\n" , optval); optval = 0 ; getsockopt (udp_sock, SOL_SOCKET, SO_TYPE, (void *)&optval, &len); printf ("udp_sock type is :%d\n" , optval); optval = 0 ; getsockopt (tcp_sock, SOL_SOCKET, SO_SNDBUF, (void *)&optval, &len); printf ("tcp_sock send buffer size is :%d\n" , optval); optval = 1024 * 16 ; setsockopt (tcp_sock, SOL_SOCKET, SO_SNDBUF, (void *)&optval, len); getsockopt (tcp_sock, SOL_SOCKET, SO_SNDBUF, (void *)&optval, &len); printf ("*tcp_sock send buffer size is :%d\n" , optval); close (tcp_sock); close (udp_sock); }
套接字的多种可选项
在客户端控制台输入Q消息,或通过CTRL+C终止程序。
在客户端输入Q消息调用close函数,向服务器端发送FIN消息并经过四次挥手的过程。
服务器端和客户端已建立连接的状态下, 向服务器端控制台输入CTRL+C,即强制关闭服务器端。
如果是CTRL+C,服务器端重新运行的时候将产生问题。如果用同一端口号重新运行服务器端,将输出“bind error” 消息,并且无法再次运行。这种情况下,大约三分钟后即可重新运行服务器端。
上述两种方法唯一的区别就是谁先传输FIN消息。
假设上图中主机A是服务器端,因为是主机A向B发送FIN消息,故可以想象成服务器端1在控制台输入CTRL+C。但问题是,套接字经过四次挥手过程后并非立即消除,而是要经过一段时间的Time-wait状态。当然,只有先断开连接的(先发送FIN消息的)主机才经过Time-wait状态。因此,若服务器端先断开连接,则无法立即重新运行。套接字处在Time-wait过程时,相应端口是正在使用的状态。因此,就像之前验证过的,bind函数调用过程中当然会发生错误。
不论是服务器端还是客户端都会有Time-wait过程。先断开连接的套接字必然会经过Time-wait过程。但是无需考虑客户端Time-wait状态。因为客户端套接字的端口号是任意指定的。
为什么会有Time-wait状态?
如上图中假设主机A向主机B传输ACK消息(SEQ5001、ACK7502)后立即消除套接字。但最后这条ACK消息在传递途中丢失,未能传给主机B。这时会发生什么?主机B会认为之前自己发送的FIN消息(SEQ 7501、ACK 5001)未能抵达主机A,继而试图重传。但此时主机A已是完全终止的状态,因此主机B永远无法收到从主机A最后传来的ACK消息。相反,若主机A的套接字处在Time-wait状态 ,则会向主机B重传最后的ACK消息,主机B也可以正常终止。基于这些考虑,先传输FIN消息的主机应经过Time-wait过程。
地址再分配
如上图所示,在主机A的四次握手过程中,如果最后的数据丢失,则主机B会认为主机A未能收到自己发送的FIN消息,因此重传。这时,收到FIN消息的主机A将重启Time-wait计时器。因此,如果网络状况不理想,Time-wait状态将持续。
解决方案就是在套接字的可选项中更改SO_REUSEADDR的状态。适当调整该参数,可将Time-wait状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR的默认值为0(假),这就意味着无法分配Time-wait状态下的套接字端口号。因此需要将这个值改成1(真)。