Linux 网络通信之TCP

介绍网络通信的使用

Posted by Jerry Chen on July 17, 2019

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

介绍

使用ss -t查看 TCP连接,使用ss -ta查看所有的包括监听状态的TCP连接。

使用ss -u查看 UDP连接,使用ss -ua查看所有的包括未连接状态的UDP连接。

使用ss -s列出所有的socket连接。

使用cat /proc/net/sockstat查看socket状态:

操作

创建socket

使用socket函数,其原型是int socket(int domain, int type, int protocol);

  • domain参数(注:下面的AF换成PF效果一样):
    • AF_UNIX/AF_LOCAL/AF_FILE: 本地通信
    • AF_INET: 网络通信 ipv4
    • AF_INET6: 网络通信 ipv6
  • type参数为通信类型
    • SOCK_STREAM: TCP
    • SOCK_DGRAM : UDP
  • protocol参数基本废弃,通常写0
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <sys/socket.h>

int main(int argc,char *argv[]){
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	printf("socket's fd is %d!\n",fd);
	while(1);
	return 0;
}

绑定socket-服务端专用

使用bind函数绑定创建的socket和地址信息。原型是int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

  • sockfd参数是socket的句柄,也就是socket函数的返回值;
  • addr参数需要用struct sockaddr_in(ipv4下)或者struct sockaddr_in(unix socket下)中转一下。这里介绍struct sockaddr_in的成员:
    • sin_family,一般写AF_INET;
    • sin_port端口,一般写htons(port),也就是需要用htons统一大小端再发出;
    • sin_addr是IP地址,它是一个联合体,一般写addr.sin_addr.s_addr = inet_addr(p);,其中p是地址的字符串,需要转换成整形。
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
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static const int port = 9999;
static const char *p = "192.168.2.246";

int main(int argc,char *argv[]){
	int sockfd;//socket的句柄
	struct sockaddr_in addr;//socket的地址端口,ipv4用这个结构体,unix socket用sockaddr_un
	int ret;
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	printf("socket's fd is %d!\n",sockfd);
	
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port); //存储中有大小端存储,这里转换为统一顺序
	addr.sin_addr.s_addr = inet_addr(p);
	
	//服务端:将地址和socket绑定
	ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1)
	{
		perror("bind");
		exit(1);
	}
	
	//客户端:根据地址连接socket,服务端需要开启监听才能由其他进程或机器连接
	ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1)
	{
		perror("connect");
		exit(1);
	}
	
	while(1);
	return 0;
}

可以看到在同一进程中建立了socket连接,虽然没啥意义。只有服务器端开启了listen才能用其他的进程或者机器进行连接,并会显示在ss -ta的LISTEN列表中。

监听socket-服务端专用

使用listen函数监听socket,开启后即可由其他进程或机器连接。原型是int listen(int sockfd, int backlog);

  • sockfd,socket函数返回的句柄;
  • backlog间接决定最大连接数,根据值不同会有一个范围,一般写128左右;

服务器端最简程序:

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static const int port = 9999;
static const char *p = "192.168.2.246";

int main(int argc,char *argv[]){
	int sockfd;//socket的句柄
	struct sockaddr_in addr;//socket的地址端口,ipv4用这个结构体,unix socket用sockaddr_un
	int ret;
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	printf("socket's fd is %d!\n",sockfd);
	
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port); //存储中有大小端存储,这里转换为统一顺序
	addr.sin_addr.s_addr = inet_addr(p);
	
	ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1)
	{
		perror("bind");
		exit(1);
	}
	
	ret = listen(sockfd, 100);//间接决定最大连接数,开始监听
	if(ret == -1)
	{
		perror("listen");
		exit(1);
	}
	
	while(1);
	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
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static const int port = 9999;
static const char *p = "192.168.2.246";

int main(int argc,char *argv[]){
	int sockfd;//socket的句柄
	struct sockaddr_in addr;//socket的地址端口,ipv4用这个结构体,unix socket用sockaddr_un
	int ret;
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	printf("socket's fd is %d!\n",sockfd);
	
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port); //存储中有大小端存储,这里转换为统一顺序
	addr.sin_addr.s_addr = inet_addr(p);
	
	ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1)
	{
		perror("connect");
		exit(1);
	}
	
	while(1);
	return 0;
}

连接socket-客户端专用

当服务器开启监听后,客户端可以使用connect函数和服务器建立连接,成功后会显示在ss -ta中,其原型是int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

  • sockfd是本地socket句柄;
  • addr是需要连接的socket地址信息,使用struct sockaddr_in补充后进行强制转换;
  • addrlen一般写sizeof(struct sockaddr_in)

可以理解为,该函数根据服务器addr把本地sockfd对应的信息发送给服务器。

连接完成后,就可以使用本地sockfd和服务器通信。

获取客户端socket地址-服务器专用

服务器使用accept函数获取客户端的地址信息,该函数会阻塞到建立连接完成

原型是int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

返回值为客户端的代理fd句柄,后面可以用write、read、send、recv操作该句柄进而和客户端通信,服务器可以有多个客户端连接。

  • sockfd是本地socket句柄;
  • addr是保存的结构体指针,使用struct sockaddr_in声明后强转填入;
  • addrlen就比较坑爹,它是个结构体指针,需要先声明结构体赋值强转填入;

获取地址后,就可以使用返回的客户端代理fd和客户端通信。

例程

通信中用到send recv其实就是高级版的write read,多了一个参数flags(为0的话就和write、read功能一样,甚至可以混用)。read、recv函数在对tcp、管道等读取时会默认阻塞,普通文件不会。

建议:

输入信息使用 fgets(buf,sizeof(buf),stdin); 这样可以保留空格;

read、recv中接收大小写sizeof(buf)

write、send中发送大小写strlen(buf) 因为声明char *buf[64];后,如果写sizeof(buf)执行write成功得到返回值始终是64;

但是以上会带来一个问题,这样每次发送的长度不等,但是是全部读取的,会造成第一次发送”abcde\n”后本地buf接收到”abcde\n”,第二次发送“123\n”后,本地buf最终值为”123\ne\n”,需要接收前清空接收buf。如果全部写sizeof(buf)就不会出现这样的问题,故有利有弊。

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>
#include <fcntl.h>
#include <string.h>

static const int port = 9999;
static const char *p = "192.168.2.246";

int main(int argc,char *argv[]){
	int ret;
	//服务器变量
	int sockfd;//socket的句柄
	struct sockaddr_in addr;//socket的地址端口,ipv4用这个结构体,unix socket用sockaddr_un

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	printf("socket's fd is %d!\n",sockfd);
	
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port); //存储中有大小端存储,这里转换为统一顺序
	addr.sin_addr.s_addr = inet_addr(p);

	ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1)
	{
		perror("bind");
		exit(1);
	}
	
	ret = listen(sockfd, 100);//间接决定最大连接数,开始监听
	if(ret == -1)
	{
		perror("listen");
		exit(1);
	}
	
	//处理客户端的变量
	int clientfd;//client socket的句柄
	struct sockaddr_in peer;//客户端地址信息保存的结构体
	socklen_t peer_len = sizeof(struct sockaddr_in);//客户端结构体大小
	char ipbuf[64];//存放客户端的ip字符串
	
	//获取连接的客户端地址信息,该函数会阻塞!!!!!
	clientfd = accept(sockfd,(struct sockaddr *)&peer,&peer_len);
	printf("client socket's fd is %d!\n",clientfd);
	//注意,下面inet_ntop函数中的参数ipbuf是必要的,不能为NULL,存放ip字符串
	printf("client ip is %s:%d\n", inet_ntop(AF_INET,&peer.sin_addr,ipbuf,64), ntohs(peer.sin_port));
	
	//处理客户端的数据
	char rbuf[1024] = "";//通信数据
	char wbuf[1024] = "";//通信数据
	int count;//接收数据长度
	while(1){
		//获取连接的客户端的数据,读管道和socket会阻塞,文件不会!!!!!这就是增强版的read函数,多了flag参数
		memset(rbuf,0,sizeof(rbuf));//必要的清接收buf,否则可能第二次接收比第一次短就有问题
		count = recv(clientfd,rbuf, sizeof(rbuf),0);//接收用sizeof
		sprintf(wbuf,"\n服务器从客户端读取到%d字节:\n%s\n",count,rbuf);
		printf("%s",wbuf);
		send(clientfd,wbuf,strlen(wbuf),0);//发送用strlen
		
		if(strncmp(rbuf,"end",3)==0)
		{
			close(clientfd);
			close(sockfd);
			exit(0);
		}
	}
	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
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>
#include <fcntl.h>
#include <string.h>

static const int port = 9999;
static const char *p = "192.168.2.246";

int main(int argc,char *argv[]){
	int ret;
	int sockfd;//socket的句柄
	struct sockaddr_in addr;//socket的地址端口,ipv4用这个结构体,unix socket用sockaddr_un

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	printf("socket's fd is %d!\n",sockfd);
	
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port); //存储中有大小端存储,这里转换为统一顺序
	addr.sin_addr.s_addr = inet_addr(p);
	
	ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));
	if(ret == -1)
	{
		perror("connect");
		exit(1);
	}

	//处理服务器的数据
	char rbuf[1024] = "";//通信数据
	char wbuf[1024] = "";//通信数据
	
	while(1){
		printf("input string:>>>\n");
		fgets(wbuf,sizeof(wbuf),stdin);//输入字符,可以有空格
		//fflush(stdin);
		write(sockfd,wbuf,strlen(wbuf));//发送用strlen
		memset(rbuf,0,sizeof(rbuf));//必要的清接收buf,否则可能第二次接收比第一次短就有问题
		read(sockfd,rbuf, sizeof(rbuf));//接收用sizeof
		printf("%s",rbuf);
		
		if(strncmp(wbuf,"end",3)==0)
		{
			close(sockfd);
			exit(0);
		}
	}
	return 0;
}

每次客户端输入信息会发送给服务器,服务器会把信息返回回来,输入”end”关闭socket连接。

只执行server进程,server阻塞在accept函数:

在另一个终端中执行client进程,会阻塞在fgets函数:

会发现server进程打印客户端地址信息,然后阻塞在recv函数:

在客户端中发送”hello world”(11+回车共12字节),client进程下面的文字是服务器返回的,server进程的文字是服务器接收的: