TCP+IPv4 客户端 & 服务端 程序简例
本文给出基于IPv4的TCP客户端以及服务端程序样例。
服务端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BACKLOG 16
int listen_fd = -1;
void sigINT(int signo);
int main()
{
if (signal(SIGINT, sigINT) == SIG_ERR)
{
printf("set signal handler(SIGINT) error!!!\n");
exit(1);
}
// socket
if ( (listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
{
printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
// bind
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(12500); // Port
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
printf("socket bind error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
// listen
if (listen(listen_fd, BACKLOG) < 0)
{
printf("socket listen error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
printf("server init ok, start to accept new connect...\n");
while (1)
{
// accept
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd < 0)
{
printf("socket accept error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
printf("accept one new connect(%d)!!!\n", client_fd);
static const char *msg = "Hello, Client!\n";
if (write(client_fd, msg, strlen(msg)) != strlen(msg))
{
printf("send msg to client error!!!\n");
}
close(client_fd);
}
}
void sigINT(int signo)
{
printf("catch SIGINT, quit...\n");
close(listen_fd);
exit(0);
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd < 0)
{
printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12500);
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0)
{
printf("inet_pton error!!!\n");
exit(1);
}
if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
printf("socket connect error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
printf("connect to server ok!\n");
char msg[1024];
int rbytes = -1;
while ( (rbytes = read(client_fd, msg, sizeof(msg)-1)) > 0)
{
msg[rbytes] = 0; // null terminate
printf("%s\n", msg);
}
if (rbytes < 0)
{
printf("read error=%d(%s)!!!\n", errno, strerror(errno));
}
exit(0);
}
服务端注意事项:
一、server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
INADDR_ANY:在服务端套接字listen前,要先进行bind,也就是将一个确定的服务端的“协议地址”与服务端listen套接字绑定。
通常“协议地址”包含三部分:协议族、IP以及PORT(服务端口)。
其中INADDR_ANY是用来设置“协议地址”中IP部分。
若协议地址的IP部分设置为INADDR_ANY,并且服务端所在主机有多个网络接口(多个IP),则服务端可以在任意网络接口(任意IP)上接收客户端链接。
例如服务端所在的主机有三个IP:
10.1.1.1
10.1.1.2
10.1.1.3
如果设置“协议地址”的IP部分为INADDR_ANY(PORT=12500),则:
1)有客户端链接10.1.1.1:12500,服务端可以收到SYN包并建立链接;
2)有客户端链接10.1.1.2:12500,服务端可以收到SYN包并建立链接;
3)有客户端链接10.1.1.3:12500,服务端可以收到SYN包并建立链接;
如果设置“协议地址”的IP部分为10.1.1.3(PORT=12500),则:
1)有客户端链接10.1.1.1:12500,服务端不可以收到SYN包并建立链接;
2)有客户端链接10.1.1.2:12500,服务端不可以收到SYN包并建立链接;
3)有客户端链接10.1.1.3:12500,服务端可以收到SYN包并建立链接;
二、服务端accept到的客户端sockfd都为4
运行服务端程序并且运行三次客户端程序,可以发现服务端有以下输出:
[jiang@localhost 0406]$ ./server
server init ok, start to accept new connection...
accept one new connect(4)!!!
accept one new connect(4)!!!
accept one new connect(4)!!!
^Ccatch SIGINT, quit...
为啥每次accept返回的客户端的sockfd都相同呢?
首先服务端启动时,fd已经有三个(0=标准输入;1=标准输出;2=标准错误),然后通过socket接口创建了listen_fd(3)。
然后通过accept,从已完成三次握手的队列中取出一个新的客户端链接,创建一个fd,此fd即为4。
服务端每次的行为都一样,先accept生成fd(4),然后写入数据到fd=4中,然后close(fd=4)。
在close后,当前服务端进程中的fd只剩下4个(0、1、2、3),fd=4又变为可用项。
当服务端再次accept生成新的fd时,内核总是从最低可用的fd开始创建占用,由于当前服务端进程有四个fd有各自用途,自然从fd=4重新开始。
每次都是close(fd=4),释放fd=4的状态为可用fd,然后accept时创建fd=4,指代新的客户端socket,往复循环。
客户端注意事项:
三、客户端read时可否不用循环读取?
在客户端connect链接服务端成功后,开始从中read数据,直到read不大于0。
read返回值:
1)>0:读到返回值大小的字节数据;
2)=0:对端关闭连接;
3)<0:read出现错误;
由于TCP是一种“数据流”,服务端写数据,客户端读数据,那客户端怎么知道啥时候“服务端要发送的全发送完毕啦!”这个信息呢?
我们在这里约定,当服务端将要发送的数据全部发送完毕时,就close客户端,通知客户端:我(服务端)要发送给你(客户端)的数据已经发送完毕啦!客户端read到0,得到了这个信息,于是乎就不再(能)继续读。当然,可以由客户端不断从收到的数据中检查某些约定好的“协议控制字符(组)”(这里的协议控制字符可以是任何字符,并非传统意义上的控制字符),例如,约定读取到两个\n就算做一个记录已经被读取完毕,开始下一个记录…即:人为约定、界定一条有效数据(或称为一条记录)的开始和结束。
更何况,万一在read时有信号来了,打断了read调用,导致只从内核接收缓冲区中读取了部分字节到用户态,仅仅调用一次read函数,不管缓冲区中是否有剩余数据,岂不是会导致数据丢失?
其实到这里已经有点偏离主题了…本意是给出一份客户端、服务端关于Socket API的使用样例,但是…
我上面的程序距离真正可用、能用、实用还有很多路要走,存在有很多可优化的地方,比如“SO_REUSEADDR ”等…