TCP客户端程序的函数调用顺序为:socket -> connect -> send/recv
socket、send和recv函数在TCP服务器程序中已经说过了,这里就不赘述了。
connect
connect函数的原型为:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket函数返回的套接字描述符
addr:需要连接的IP地址和端口号,但一般传入struct sockaddr_in类型的指针
addrlen:传入sizeof(struct sockaddr_in)
struct sockaddr_in结构定义如下:
struct sockaddr_in{ sa_family_t sin_family; /* 地址族(Address Family)*/ uint16_t sin_port; /* 端口号 */ struct in_addr sin_addr; /* IP地址 */ char sin_zero[8]; /* 不使用,一般用0填充 */ };
struct in_addr结构定义如下:
struct in_addr{ in_addr_t s_addr; /* 32位的IP地址 */ };
同样的,对于TCP客户端而言,sin_family的值为AF_INET,sin_addr.s_addr写入服务器的IP地址,用inet_addr函数转换,sin_port写入服务器的端口,用htons函数转换。
connect函数成功返回0,失败返回-1。
connect成功之后便可用recv和send函数收发数据了,同样的,如果服务器断开连接,那么recv函数将不再阻塞,返回值为0,可以通过recv的返回值判断服务器是否断开连接。
不同的是调用recv和send时,TCP服务器程序传入的sockfd为accept返回的值,而TCP客户端程序传入的直接就是socket函数的返回值。
有一个很值得重视的问题,网上所有关于connect的解释都说这个函数默认是阻塞的,而我在测试过程中却发现下面的问题:
1. 在调用socket函数得到sockfd之后,不对其进行设置,在服务器不开启(能ping通)的情况下,执行connect会在大概3秒之后返回-1。
2. 在调用socket函数得到sockfd之后,不对其进行设置,给connect传入的IP是同网段下,执行connect会立即返回-1。
3. 在调用socket函数得到sockfd之后,将sockfd设置为非阻塞,无论在服务器是否打开的情况下,执行connect会立即返回-1。
4. 在connect成功之后将sockfd设置为非阻塞,此时recv变为非阻塞。
这几条测试结果与网上的说法完全不同,首先如果在socket之后,connect之前将sockfd设置为非阻塞,那么不管怎样connect都将失败,但是在socket之后,connect之前将sockfd设置为阻塞,此时如果能连接服务器,则立刻返回0,否则立刻返回-1,这与网上说connect会阻塞是矛盾的。只有在connect成功之后将sockfd设置为非阻塞,recv才能变为非阻塞,而connect函数是无论如何都是非阻塞的。
测试程序如下:
1 /** 2 * filename: tcp_client.c 3 * author: Suzkfly 4 * date: 2021-01-22 5 * platform: Ubuntu 6 * 配合windows的网络调试工具使用: 7 * 1、先保证windows与Ubuntu在同一网段且互相能ping通; 8 * 2、在windows下打开网络调试助手,选择协议类型为TCP Server,本地主机地址选 9 * 择windows的IP地址(或者windows下能和Ubuntu ping通的地址),端口号和 10 * 本文件中传入的端口号一致,接收设置和发送设置都选择ASCLL。 11 * 3、点击“打开”按钮。 12 * 4、运行Ubuntu下的TCP客户端程序; 13 * 5、连接成功后在网络调试助手上发送数据,在Ubuntu下的终端上能看到, 14 * 在Ubuntu下的终端上输入字符串按回车发送,在windows上的网络调试助手上也 15 * 能看到。 16 */ 17 #include <stdio.h> 18 #include <sys/types.h> 19 #include <sys/socket.h> 20 #include <string.h> 21 #include <netinet/in.h> 22 #include <arpa/inet.h> 23 24 #define IP_ADDR "192.168.0.1" /* 服务器IP地址 */ 25 #define PORT 24576 /* 服务器端口号 */ 26 27 28 int main(int argc, const char *argv[]) 29 { 30 int sock_fd = 0; 31 int ret = 0; 32 struct sockaddr_in serv_addr; /* 服务器地址 */ 33 int pid = 0; 34 char buf[128] = { 0 }; 35 int len = 0; 36 37 /* 创建TCP套接字 */ 38 sock_fd = socket(AF_INET, SOCK_STREAM, 0); 39 if (sock_fd < 0) { 40 printf("socket failed\n"); 41 return 0; 42 } 43 44 /* 与服务器建立连接 */ 45 memset(&serv_addr, 0, sizeof(struct sockaddr_in)); 46 serv_addr.sin_family = AF_INET; 47 serv_addr.sin_addr.s_addr = inet_addr(IP_ADDR); /* 服务器IP */ 48 serv_addr.sin_port = htons(PORT); /* 服务器端口号 */ 49 ret = connect(sock_fd, 50 (struct sockaddr *)&serv_addr, 51 sizeof(struct sockaddr_in)); 52 if (ret == 0) { 53 printf("connect ok\n"); 54 } else { 55 printf("connect failed\n"); 56 close(sock_fd); 57 return 0; 58 } 59 60 pid = fork(); 61 62 if (pid > 0) { /* 接收数据 */ 63 while (1) { 64 memset(buf, 0, sizeof(buf)); 65 len = recv(sock_fd, buf, sizeof(buf), 0); 66 67 if (len == 0) { /* 如果recv返回0,则表示远端断开连接 */ 68 break; 69 } 70 71 printf("len = %d\n", len); 72 printf("data: %s\n", buf); 73 } 74 } else if (pid == 0) { /* 发送数据 */ 75 while (1) { 76 memset(buf, 0, sizeof(buf)); 77 scanf("%s", buf); 78 send(sock_fd, buf, strlen(buf), 0); 79 } 80 } 81 }
该程序在服务器断开连接的情况下,客户端不进行重连,而是直接退出程序。
该程序存在一个bug,就是发送数据和接收数据是通过不同的进程来控制的,而当服务器断开连接后,接收进程结束了,但发送进程还未结束。这个问题在https://www.cnblogs.com/Suzkfly/p/14326811.html这篇博客中解决了。
网络调试助手设置如下: