C语言实现TCP/IP连接

利用C语言,实现TCP/IP连接。其中采用CS模式+套接字的方式。

先上代码:

Server.c 服务端

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Server.c

#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <ws2tcpip.h>
#include <stdio.h>

#define SERVER_PORT 6666

// 套接字连接--全局
int client;
// 调用socket函数返回的文件描述符
int serverSocket;
unsigned char buffer[200]; //存储 发送和接收的信息
int iDataNum;


// 初始化服务端,等待客户端连接
int sockInit()
{
/*
监听后,一直处于accept阻塞状态,
直到有客户端连接,
当客户端如数quit后,断开与客户端的连接
*/

//声明两个套接字sockaddr_in结构体变量,分别表示客户端和服务器
struct sockaddr_in server_addr;
struct sockaddr_in clientAddr;

int addr_len = sizeof(clientAddr);

//必须先初始化
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("require version fail!");
return -1;
}

//socket函数,失败返回-1
//int socket(int domain, int type, int protocol);
//第一个参数表示使用的地址类型,一般都是ipv4,AF_INET
//第二个参数表示套接字类型:tcp:面向连接的稳定数据传输SOCK_STREAM
//第三个参数设置为0
//建立socket
if ((serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}

//初始化 server_addr
memset(&server_addr, 0, sizeof(server_addr));

//初始化服务器端的套接字,并用htons和htonl将端口和地址转成网络字节序
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);

//ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

//对于bind,accept之类的函数,里面套接字参数都是需要强制转换成(struct sockaddr *)
//bind三个参数:服务器端的套接字的文件描述符,
if (bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("connect");
return 1;
}

//设置服务器上的socket为监听状态
if (listen(serverSocket, 5) < 0)
{
perror("listen");
return 1;
}
while (1)
{
printf("\n监听端口: %d\n", SERVER_PORT);
//调用accept函数后,会进入阻塞状态
//accept返回一个套接字的文件描述符,这样服务器端便有两个套接字的文件描述符,
//serverSocket和client。
//serverSocket仍然继续在监听状态,client则负责接收和发送数据
//clientAddr是一个传出参数,accept返回时,传出客户端的地址和端口号
//addr_len是一个传入-传出参数,传入的是调用者提供的缓冲区的clientAddr的长度,以避免缓冲区溢出。
//传出的是客户端地址结构体的实际长度。
//出错返回-1
client = accept(serverSocket, (struct sockaddr *)&clientAddr, (socklen_t *)&addr_len);
if (client < 0)
{
perror("accept");
continue;
}

// inet_ntoa ip地址转换函数,将网络字节序IP转换为点分十进制IP
// 表达式:char *inet_ntoa (struct in_addr);
// printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr)); //把来访问的客户端的IP地址打出来
// printf("Port is %d\n\n", htons(clientAddr.sin_port));
printf("host: %s:%d\n\n", inet_ntoa(clientAddr.sin_addr), htons(clientAddr.sin_port));
break;
}
}

void get_msg(){
while (1)
{
// 接收数据
memset(buffer, 0, sizeof(buffer));
printf("等待信息...\n");
while (1)
{
iDataNum = recv(client, buffer, sizeof(buffer), 0);
if (iDataNum >= 0)
break;
}
buffer[iDataNum] = '\0';
printf("收到的信息:%s\n\n",buffer);

// 发送信息
memset(buffer, 0, sizeof(buffer));
printf("请输入发送的内容:");
scanf("%s",&buffer);
send(client, buffer, strlen(buffer), 0); //向服务端发送消息

printf("\n\n");
}
}

int main()
{
sockInit();
get_msg();
getchar();
close(serverSocket);

return 0;
}

Client.c 客户端

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
85
86
87
88
89
90
//Client.c

#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <ws2tcpip.h>
#include <stdio.h>

#define SERVER_PORT 6666

// 套接字连接--全局
// 客户端只需要一个套接字文件描述符,用于和服务器通信
int serverSocket;
unsigned char buffer[200]; //存储 发送的信息
int iDataNum;

// 连接服务端
int sockInit()
{

//描述服务器的socket
struct sockaddr_in serverAddr;

//下面代码初始化
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("require version fail!");
return -1;
}

if ((serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
{
perror("socket");
return 1;
}

serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERVER_PORT);

//指定服务器端的ip,本地测试:127.0.0.1
//inet_addr()函数,将点分十进制IP转换成网络字节序IP
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

if (connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
{
perror("connect");
return 1;
}
return 1;
}

int main()
{
if (sockInit() != 1)
return -1;

printf("\n连接成功...\n\n");

while(1){

// 发送信息
memset(buffer, 0, sizeof(buffer));
printf("请输入发送的内容:");
scanf("%s",&buffer);
send(serverSocket, buffer, strlen(buffer), 0); //向服务端发送消息

printf("\n\n");

// 接收数据
memset(buffer, 0, sizeof(buffer));
printf("等待信息...\n");
while (1)
{
iDataNum = recv(serverSocket, buffer, sizeof(buffer), 0);
if (iDataNum >= 0)
break;
}
buffer[iDataNum] = '\0';
printf("收到的信息:%s\n\n",buffer);

}

getchar();
close(serverSocket);

return 0;
}

其中,send()是用来发送数据的,recv()是用来接收数据的

接收数据的时候,通常情况下是不知道大小的,所以使用的是缓冲区的大小sizeof(buffer)

但在发送数据的时候,是可以知道数据的长度/大小的,所以可以直接用*strlen(buffer)*来获取和确定要发送的长度

注:直接在cmd用gcc编译时,记得加上 -lwsock32 (当前是Windows 10)

$ gcc .\Server.c -o .\Server.exe -lwsock32


注意

还有一点需要注意的是,我试过:先写好客户端只发送数据,服务端只接收数据

然后利用Client端 连续不间断地向Server端发送长度较大的数据,这时在Server端打印接收到的数据时,往往会显示(接收)不完全,或许是recv()中的数据并没有被完全读进buffer中去。

怎么说呢,就像是我Server端要接收一大串的数据,我这一大串(来自recv的)数据还没有读到尾(‘\0’) 内存还没读完,buffer就要被拿去printf了,printf完就被填充全0,准备下一次的数据接收。

那怎么办呢,要确保Server端完整读取发送过来的数据呀,那我加延时呗 等内存读完呗,延时完再打印和初始化buffer。

实验结果发现,确实可以,但!这个延时加在Server端却没用,对于Server端来说 每一条语句执行的顺序都是一样的,速度也是相当的,你不可能说加了个延时它就不接收数据了呀,对吧。

然后我把延时加在了Client端发送那里,确保每次发送都有一段不会太短的时间间隔,这样Server端就能完整读取完数据再处理下一次的接收。

真是非常的amazing啊,当初我还是搞网安玩加密的时候试出来的,调了半天,以为是因为加密后乱码 发过去了读不出来,结果才发现可能是内存没读完导致的,郁闷了两天(托腮.jpg)

不过这也只不过是我猜测的啦,真正具体是什么原因导致读不完我还不怎么清楚了啦……(蹲个大佬 我插个眼)