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进程的文字是服务器接收的: