Linux——TCP通信实例

本文最后更新于:2021年4月14日 晚上

概览:LinuxTCP通信实例

LinuxTCP通信简单版

实例:客户端获取用户的输入发送给服务端,服务端接收消息后,回送一条信息给客户端,并将两条消息保存至文件!

服务器端

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>


char* message[11]= {"你是否会想起,我哪天都不想起",
"天生我材必有用,千金散尽还复来",
"床前明月光,疑是地上霜",
"清晨回笼做了梦",
"我们无话可说,但必须打破沉默",
"弃我去者,昨日之日不可留",
"艰难苦恨繁霜鬓,潦倒新停浊酒杯",
"白山黑水妖颜",
"我将已学会爱黑暗日子同光明日子一样,爱黑的雨白的山",
"而从前我只爱我的幸福和你",
"此刻你若不爱我,我也不会介意"};

int main(){

//1. 创建监听的socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1){
perror("socket");
exit(-1);
}

//2. 绑定Ip和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
inet_pton(AF_INET,"172.26.96.221",&saddr.sin_addr.s_addr);
saddr.sin_port = htons(9876);
int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}

//3.监听
ret = listen(lfd,8);
if(ret == -1){
perror("listen");
exit(-1);
}

//4.连接
struct sockaddr_in clientaddr;
clientaddr.sin_family = AF_INET;
socklen_t len = sizeof(clientaddr);
int afd = accept(lfd,(struct sockaddr *)&clientaddr,&len);
if(afd == -1){
perror("accept");
exit(-1);
}

//5. 打印客户端连接的Ip和端口
char clientIp[16];
inet_ntop(afd,&clientaddr.sin_addr.s_addr,clientIp,sizeof(clientIp));
unsigned short clientPort = ntohs(clientaddr.sin_port);

printf("link..,client ip : %s ,port: %d\n",clientIp,clientPort);

//6. 接收客户端发来的消息,随机回复一条消息。
char recvBuf[1024] = {0};
int filefd = open("chat.txt",O_WRONLY|O_CREAT|O_APPEND,0664);
if(filefd == -1){
perror("open file");
exit(-1);
}

srand((unsigned)time( NULL ));

while(1){

bzero(recvBuf,sizeof(recvBuf));
int rlen = read(afd,recvBuf,sizeof(recvBuf));
if(rlen == -1){
perror("read");
exit(-1);
}else if(rlen > 0){
printf("client: %s\n",recvBuf);
}else if(rlen == 0){
printf("client close...\n");
break;
}

//回送消息

int nums = rand()%11;

char buf[2096]= {0};
sprintf(buf,"client: %s \t server: %s \n",recvBuf,message[nums]);
write(filefd,buf,strlen(buf));//写内容给文件!
write(afd,message[nums],strlen(message[nums]));
}

close(filefd);
close(afd);
close(lfd);

return 0;
}

客户端

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

int main(){

//1. 创建socket
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){
perror("socket");
exit(-1);
}

//2. 连接connect
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9876);
inet_pton(AF_INET,"172.26.96.221",&addr.sin_addr.s_addr);
int ret = connect(fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret == -1){
perror("connect");
exit(-1);
}

printf("link server success!\n");

char recvBuf[1024] = {0};
char inputBuf[1024] = {0};
while(1){
bzero(inputBuf,sizeof(inputBuf));
printf("please input some message:");
gets(inputBuf);
//3. 获取用户输入,发送给服务器
write(fd,inputBuf,strlen(inputBuf));

//4.接收消息,显示出来,然后继续等待用户输入
bzero(recvBuf,sizeof(recvBuf));
int rlen = read(fd,recvBuf,sizeof(recvBuf));
if(rlen == -1){
perror("read");
exit(-1);
}else if(rlen > 0){
printf("server:%s\n",recvBuf);
}else if(rlen == 0){
printf("serveer close...\n");
break;
}
}

close(fd);

return 0;
}

运行效果——客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
aliyun@ali-colourso:~/learn/06tcp$ ./chcl
link server success!
please input some message:你好世界!
server:艰难苦恨繁霜鬓,潦倒新停浊酒杯
please input some message:哈哈哈哈
server:此刻你若不爱我,我也不会介意
please input some message:哦吼!
server:艰难苦恨繁霜鬓,潦倒新停浊酒杯
please input some message:还有别的吗?
server:床前明月光,疑是地上霜
please input some message:再来?
server:天生我材必有用,千金散尽还复来
please input some message:??
server:我们无话可说,但必须打破沉默
please input some message:打扰了
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
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}

// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "172.26.96.221", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9876);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

if(ret == -1) {
perror("connect");
exit(-1);
}

// 3. 通信
char recvBuf[1024];
int i = 0;
while(1) {

sprintf(recvBuf, "data : %d\n", i++);

// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);

int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}

sleep(1);
}

// 关闭连接
close(fd);

return 0;
}

服务器实现

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
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main() {

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9876);
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}

// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}

// 不断循环等待客户端连接
while(1) {

struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}

// 每一个连接进来,创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(cfd, &recvBuf, sizeof(recvBuf));

if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
exit(0); // 退出当前子进程
}

}
close(lfd);
return 0;
}
  • 问题所在!:没有对子进程进行回收,会不断的消耗资源!
  • 如何对子进程进行回收?
    • wait:wait是阻塞函数!会阻塞等待,影响主进程的运行!
    • waitpid 阻塞版本也不行
    • waitpid NOHANG,非阻塞版本倒是可以,不推荐采用
    • 使用信号回收!子进程结束后会通过内核发送一个SIGCHLD信号,通过信号捕捉达到回收的目的!

服务器改进

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
void recyleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
}else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0){
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}

int main() {

struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);


// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}

// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}

// 不断循环等待客户端连接
while(1) {

struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}

// 每一个连接进来,创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(cfd, &recvBuf, sizeof(recvBuf));

if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
exit(0); // 退出当前子进程
}

}
close(lfd);
return 0;
}
  • 回收了子进程,但是还有新的问题!
  • 信号捕捉是一种软中断,当发生软中断之后,accept会发生一个EINTR的错误,从而不会再继续执行!
  • 解决办法:对于这种错误,忽略其执行
1
2
3
4
5
6
7
8
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) {
continue;
}
perror("accept");
exit(-1);
}

EINTR错误

当程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,在信号处理函数返回后,程序将面临继续执行或不执行慢速系统调用两种选择,默认情况下是系统调用将被中断,并且errno被设置为EINTR。我们可以选择继续执行,有以下两种方法:

1.在设置信号处理函数的时候,为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用,但是该方法对某些慢速系统调用无效,比如epoll_wait,poll,seletc等慢速系统调用,即使给信号设置了该选项,也会被中断。具体对那些慢速系统调用无效可查看manpages。

2.手动重启,对慢速系统调用的返回值进行判断,若errno==EINTR,则重新调用慢速系统调用

综上所述,若想要重启系统调用,最好使用方法2。
————————————————
版权声明:本文为CSDN博主「城南花已开.jpg」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_39781096/article/details/107046804

多线程实现服务器并发

服务器实现

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>

struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr;
pthread_t tid; // 线程号
};

//这种全局变量,对线程来说是共享的!
struct sockInfo sockinfos[24];


void * work(void * arg){
//通过arg获得一些信息,包括tcp连接的socket以及地址信息
struct sockInfo * pinfo = (struct sockInfo *)arg;

char cliIp[16];
inet_ntop(AF_INET,&pinfo->addr.sin_addr,cliIp,sizeof(cliIp));
unsigned short cliPort;
cliPort = ntohs(pinfo->addr.sin_port);

printf("client ip is: %s, port is : %d\n",cliIp,cliPort);

//接收客户端发来的消息!
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
}
close(pinfo->fd);
return NULL;
}

int main(){

//1. 创建监听的socket
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1){
perror("socket");
exit(-1);
}

//2. 绑定Ip和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
inet_pton(AF_INET,"172.26.96.221",&saddr.sin_addr.s_addr);
saddr.sin_port = htons(9876);
int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}

//3.监听
ret = listen(lfd,8);
if(ret == -1){
perror("listen");
exit(-1);
}

//初始化sockinfos
int infolen = sizeof(sockinfos)/sizeof(sockinfos[0]);
for(int i=0;i<infolen;i++){
bzero(&sockinfos[i],sizeof(sockinfos[0]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}

//4.连接
while(1){

struct sockaddr_in clientaddr;
clientaddr.sin_family = AF_INET;
socklen_t len = sizeof(clientaddr);
int afd = accept(lfd,(struct sockaddr *)&clientaddr,&len);
if(afd == -1){
perror("accept");
exit(-1);
}

struct sockInfo * pinfo;

//从数组中找到一个可用的
for(int i=0;i<infolen;i++){
if(sockinfos[i].fd == -1){
pinfo = &sockinfos[i];
break;
}
}

pinfo->fd = afd;
memcpy(&pinfo->addr,&clientaddr,len);

//连接成功
//创建子线程来处理逻辑业务
pthread_create(&pinfo->tid,NULL,work,pinfo);
//需要给子线程通过第四个参数传递信息,包括建立连接的socket,等

pthread_detach(pinfo->tid);//分离线程,如果join连接线程,就会一直阻塞在这里

}

return 0;
}

注意结构体的思想:

  • struct sockInfo以及其对应的一个全局数组,这样便于多个线程去传递参数,又不至于开辟内存到堆上,还得额外处理释放的问题!!

客户端实现

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
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}

// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "172.26.96.221", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9876);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

if(ret == -1) {
perror("connect");
exit(-1);
}

// 3. 通信
char recvBuf[1024];
int i = 0;
while(1) {

sprintf(recvBuf, "data : %d\n", i++);

// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);

int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}

sleep(1);
}

// 关闭连接
close(fd);

return 0;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!