Linux下的Socket编程

主要函数

int socket(int domain, int type, int protocol)

创建一个用于网络通信的socket套接字,唯一标识一个socket,后续的网络通信会以它为参数,来进行一些通信操作。

参数:

  • domain:协议族,它决定了socket的地址类型,在通信中必须采用对应的地址。常用的协议族有AF_INET、AF_INET6、AF_UNIX、AF_ROUTE等等。例如,AF_INET决定了要使用ipv4地址(32位)与端口号(16位)的组合;AF_UNIX决定了要用一个绝对路径名作为地址。

  • type:socket类型,常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。例如,SOCK_STREAM决定了套接字类型为流套接字,适用于TCP协议。

  • protocol:协议类别,常见的有0、IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等等。例如,设为0,会自动选择type类型对应的默认协议;IPPROTO_TCP决定协议类别为TCP协议。

返回值:

  • 成功:socket描述符。
  • 失败:-1,并将errno设置为对应的错误。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

将套接字与特定的IP地址和端口绑定起来,以便套接字处理之后流经该IP地址和端口的数据。

参数:

  • sockfd:socket描述符,通过socket()函数创建,唯一标识一个socket。bind()函数将这个描述字绑定一个名字。

  • addr:const struct sockaddr *指针,指定绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,IPv4和IPv6就有所差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//IPv4:
struct sockaddr_in {
sa_family_t sin_family; // 地址类型,取值AF_INET
in_port_t sin_port; // 网络字节序端口号
**struct** in_addr sin_addr; // IP地址
};
// IP地址
struct in_addr {
uint32_t s_addr; // 网络字节序地址
};

//IPv6:
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址类型,取值AF_INET6
in_port_t sin6_port; // 网络字节序端口号
uint32_t sin6_flowinfo; // IPv6 流信息
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 接口范围
};
// IPv6地址
struct in6_addr {
unsigned **char** s6_addr[16]; // IPv6 网络字节序地址
};
  • addrlen:socket地址的长度。

int listen(int sockfd, int backlog)

将socket由主动类型变为被动类型,等待客户的连接请求,并使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接。

参数:

  • sockfd:bind()绑定之后的socket描述符。
  • backlog:相应socket连接队列的长度,即可以排队等待的最大连接个数。

返回值:

  • 成功:0。
  • 失败:-1,并将errno设置为对应的错误。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

客户端通过调用connect()函数来建立与TCP服务器的连接。

参数:

  • sockfd:socket()函数返回的socket描述符。
  • addr:指定被连接的服务器端地址和端口信息。
  • addrlen:socket地址的长度。

返回值:

  • 成功:0.
  • 失败:-1,并将errno设置为对应的错误。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

由TCP服务器调用,响应连接请求,建立与client的连接。

参数:

  • sockfd:listen()函数监听后的通过socket描述符。
  • addr:返回已连接客户端的IP、端口等信息。
  • addrlen:返回真实addr所指向结构的大小。

返回值:

  • 成功:非负的,客户端和服务端之间进行数据传输的socket描述符。

  • 失败:-1,并将errno设置为对应的错误。

ssize_t read(int filedes, void* buf, size_t nbytes)

读取文件中的数据,并把它放到数据缓存区中。

参数:

  • filedes:文件描述符,指定所需要读取的文件。
  • buf:数据缓存区,指定读取后文件内容所要存放的区域。
  • nbytes:字节数,指定所要读取的字节数。

返回值:

  • 成功:实际读取的字节数。
  • 失败:-1,并将errno设置为对应的错误。
  • 达到文件末尾:0。

ssize_t write(int filedes, void* buf, size_t nbytes)

将数据缓存区中的内容写入文件中。

参数:

  • filedes:文件描述符,指定所需要写入的文件。
  • buf:数据缓存区,指定要写入文件的内容所在区域。
  • nbytes:字节数,指定所要写入的字节数。

返回值:

  • 成功:成功写入的字节数。
  • 失败:-1,并将errno设置为对应的错误。

ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags)

用于TCP协议中发送数据。

参数:

  • sockfd:指定发送端的socket描述符。
  • buf:存放要发送数据的缓冲区。
  • nbytes:实际要发送的数据字节数。
  • flags:置为0或者下表中几个值。
flags 说明
MSG_DONTROUTE 绕过路由表查找
MSG_DONTWAIT 仅本操作非阻塞
MSG_OOB 发送或接收带外数据

返回值:

  • 成功:已发送的字节数。
  • 失败:-1,并将errno设置为对应的错误。

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags)

用于TCP协议中接收数据。

参数:

  • sockfd:指定接收端的socket描述符。
  • buf:指向接收数据缓冲区的指针。
  • nbytes:buf缓冲区的大小。
  • flags:置为0或者下表中几个值。
flags 说明
MSG_DONTWAIT 仅本操作非阻塞
MSG_OOB 发送或接收带外数据
MSG_PEEK 窥看外来数据
MSG_WAITALL 等待所有数据

返回值:

  • 成功:实际接收的字节数。
  • 失败:-1,并将errno设置为对应的错误。
  • 对应端已关闭:0。

int close(int sockfd)

用于TCP协议中关闭特定的socket连接。

参数:

  • socked:指定要关闭的socked描述符。

返回值:

  • 成功:0。
  • 失败:-1,并将errno设置为对应的错误。

编程实现

服务端

作为常驻程序,为客户端提供服务。能够根据客户端的不同选择,去进行信息交互或者是传输文件。

代码:

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
141
142
143
144
145
146
147
148
149
150
151
152
#include< stdio.h >       // printf  代码渲染问题,使用要去掉空格
#include< stdlib.h > // exit
#include< string.h > // bzero
#include< unistd.h > // close
#include< sys/types.h > // socket
#include< sys/socket.h > // socket
#include< netinet/in.h > // sockaddr_in

#define SERVER_PORT 6666
#define LENGTH_OF_LISTEN_QUEUE 20
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512

int main(int argc, char** argv)
{
// 1.声明并初始化一个服务器端的socket地址结构
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));//将结构体清空
server_addr.sin_family = AF_INET; //采用IPv4协议
server_addr.sin_port = htons(SERVER_PORT); //转换端口号字节序
server_addr.sin_addr.s_addr = htons(INADDR_ANY);//自动填充本机IP地址,并转换字节序


// 2.创建socket,若成功,返回socket描述符
// IPv4,STREAM类型,自动选择传输协议,这里为TCP协议
int server_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(server_socket_fd < 0){
perror("Create Socket Failed:");
exit(1);
}
// 设置套接字的选项,允许重用本地地址和端口
int opt = 1;
setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

// 3.绑定socket和socket地址结构
if((bind(server_socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))) == -1){
perror("Server Bind Failed:");
exit(1);
}

// 4.socket监听
if((listen(server_socket_fd, LENGTH_OF_LISTEN_QUEUE)) == -1){
perror("Server Listen Failed:");
exit(1);
}
printf("Listening...\n");

while(1){//循环处理
// 定义客户端的socket地址结构
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);

// 5.接受连接请求,返回一个新的socket描述符,这个新socket用于同连接的客户端通信
// accept函数会把连接到的客户端信息写到client_addr中
int new_server_socket_fd = accept(server_socket_fd, (struct sockaddr*)&client_addr, &client_addr_length);
if(new_server_socket_fd < 0){
perror("Server Accept Failed:");
break;
}

// 6.recv函数接收数据到缓冲区buffer中
char buffer[BUFFER_SIZE];
bzero(buffer, BUFFER_SIZE);
if(recv(new_server_socket_fd, buffer, BUFFER_SIZE, 0) < 0){
perror("Server Recieve Data Failed:");
break;
}

// 7.从buffer(缓冲区)拷贝到service_num中
char service_num[5];
int service_num_size = 5;
bzero(service_num, service_num_size);
strncpy(service_num, buffer, service_num_size);


if(strcmp(service_num, "1") == 0){ //收发消息
char buffer[BUFFER_SIZE];
while(1){
bzero(buffer, BUFFER_SIZE);
if(recv(new_server_socket_fd, buffer, BUFFER_SIZE, 0) < 0){
perror("Server Recieve Data Failed:");
break;
}
printf("Client said:'%s'\n", buffer);

// 清空缓冲区,用于填充发送信息
bzero(buffer, BUFFER_SIZE);
printf("Please input message to send:");
fgets(buffer, BUFFER_SIZE, stdin); //读入信息
if(buffer[strlen(buffer) - 1]=='\n'){ //因为最后一行没有回车符
buffer[strlen(buffer) - 1]='\0';
}
if(!strncasecmp(buffer,"quit",4)){
printf("I will quit!\n");
break;
}
if(send(new_server_socket_fd,buffer,strlen(buffer),0) < 0){
perror("Server Send Data Failed:");
exit(1);
}
else
printf("Message:%s\tsend successfully\n",buffer);

printf("Waiting for reply...\n");
}
close(new_server_socket_fd);
}
else if(strcmp(service_num, "2") == 0){ //传送文件给客户端
printf("%s\n", service_num);

// recv函数接收数据到缓冲区buffer中
bzero(buffer, BUFFER_SIZE);
if(recv(new_server_socket_fd, buffer, BUFFER_SIZE, 0) < 0){
perror("Server Recieve Data Failed:");
break;
}

// 从buffer(缓冲区)拷贝到file_name中
char file_name[FILE_NAME_MAX_SIZE+1];
bzero(file_name, FILE_NAME_MAX_SIZE+1);
strncpy(file_name, buffer, strlen(buffer)>FILE_NAME_MAX_SIZE?FILE_NAME_MAX_SIZE:strlen(buffer));
printf("%s\n", file_name);

// 打开文件并读取文件数据
FILE *fp = fopen(file_name, "r");
if(NULL == fp){ //这种情况,服务端找不到文件,没有信息发送,客户端会接收到请求文件名的空白文件
printf("File:%s Not Found\n", file_name);
}
else{
bzero(buffer, BUFFER_SIZE);
int length = 0;
// 每读取一段数据,便将其发送给客户端,循环直到文件读完为止
while((length = fread(buffer, sizeof(char), BUFFER_SIZE, fp)) > 0){
if(send(new_server_socket_fd, buffer, length, 0) < 0){
printf("Send File:%s Failed./n", file_name);
break;
}
bzero(buffer, BUFFER_SIZE);
}

// 关闭文件
fclose(fp);
printf("File:%s Transfer Successfully!\n", file_name);
}
// 关闭与客户端的连接
close(new_server_socket_fd);
}
}
// 关闭监听用的socket
close(server_socket_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
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#include< stdio.h >        // printf  
#include< stdlib.h > // exit
#include< string.h > // bzero
#include< unistd.h > // close
#include< sys/types.h > // socket
#include< sys/socket.h > // socket
#include< netinet/in.h > // sockaddr_in
#include< arpa/inet.h > // inet_pton

#define SERVER_PORT 6666
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512

int main(int argc, char** argv)
{
// 1.声明并初始化一个客户端的socket地址结构
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_addr.s_addr = htons(INADDR_ANY);
client_addr.sin_port = htons(0);

// 2.创建socket,若成功,返回socket描述符
int client_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(client_socket_fd < 0){
perror("Create Socket Failed:");
exit(1);
}

// 3.绑定客户端的socket和客户端的socket地址结构 非必需
if(-1 == (bind(client_socket_fd, (struct sockaddr*)&client_addr, sizeof(client_addr)))){
perror("Client Bind Failed:");
exit(1);
}

// 4.声明一个服务器端的socket地址结构,并用服务器那边的IP地址及端口对其进行初始化,用于后面的连接
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
if(inet_pton(AF_INET, "192.168.123.137", &server_addr.sin_addr) == 0){
perror("Server IP Address Error:");
exit(1);
}
server_addr.sin_port = htons(SERVER_PORT);
socklen_t server_addr_length = sizeof(server_addr);

// 5.向服务器发起连接,连接成功后client_socket_fd代表了客户端和服务器的一个socket连接
if(connect(client_socket_fd, (struct sockaddr*)&server_addr, server_addr_length) < 0){
perror("Can Not Connect To Server IP:");
exit(0);
}

// 6.选择传输短信息还是传输文件
char service_num[5];
bzero(service_num, strlen(service_num));
printf("Please choose the serial number before the service you want to use:\n");
printf("1.Transfer short messsage.\n");
printf("2.Transfer file.\n");
printf("3.quit\n");
scanf("%s", service_num);

char buffer[BUFFER_SIZE];
bzero(buffer, BUFFER_SIZE);
strncpy(buffer, service_num, strlen(service_num)>BUFFER_SIZE?BUFFER_SIZE:strlen(service_num));

// 7.向服务器发送buffer中的数据
if(send(client_socket_fd, buffer, BUFFER_SIZE, 0) < 0){
perror("Send File Name Failed:");
exit(1);
}

// 8.根据输入序号来选择服务
if(strcmp(service_num, "1") == 0){ //收发消息
printf("Now, you can comunicate with server.And you can quit with inputing 'quit'.\n");
char bin[5]; //吞掉前面这个回车
fgets(bin, 5, stdin);
while(1){
bzero(buffer, BUFFER_SIZE);
printf("Please input the messsage you want to send: ");
fgets(buffer, BUFFER_SIZE, stdin); //读入信息
if(buffer[strlen(buffer) - 1]=='\n'){ //去掉最后的回车符
buffer[strlen(buffer) - 1]='\0';
}

// 检测是否退出
if(!strncasecmp(buffer,"quit",4)){
printf("I will close the connect!\n");
break;
}

// 发送数据给服务器
if(send(client_socket_fd, buffer, strlen(buffer), 0) < 0){
perror("Client Send Message Failed:");
exit(1);
break;
}
else
printf("Message:%s\t send sucessfully\n", buffer);

// 等待
printf("Waiting for reply...\n");

// 清空缓冲区,用于填充接收信息
bzero(buffer, BUFFER_SIZE);
if(recv(client_socket_fd, buffer, BUFFER_SIZE, 0) < 0){
perror("Client Receive Data Failed:");
exit(1);
}
else
printf("Server said:'%s'\n", buffer);
}
}
else if(strcmp(service_num, "2") == 0){ //从服务器获取文件
while(1){
// 输入文件名
char file_name[FILE_NAME_MAX_SIZE+1];
bzero(file_name, FILE_NAME_MAX_SIZE+1);
printf("Please input file name on server: ");
scanf("%s", file_name);

// 将文件名放到缓冲区buffer中等待发送
char buffer[BUFFER_SIZE];
bzero(buffer, BUFFER_SIZE);
strncpy(buffer, file_name, strlen(file_name)>BUFFER_SIZE?BUFFER_SIZE:strlen(file_name));

// 向服务器发送buffer中的数据
if(send(client_socket_fd, buffer, BUFFER_SIZE, 0) < 0){
perror("Send File Name Failed:");
exit(1);
}

// 打开文件,准备写入
char file[520];
sprintf(file, "/Users/crownz/Desktop/网络通信/%s", file_name);
FILE *fp = fopen(file, "w");
if(NULL == fp){
printf("File:\t%s Can't Open To Write\n", file);
exit(1);
}

// 从服务器接收数据到buffer中,并将其写入文件中
// 每接收一段数据,便将其写入文件中,循环直到文件接收完并写完为止
bzero(buffer, BUFFER_SIZE);
int length = 0;
while((length = recv(client_socket_fd, buffer, BUFFER_SIZE, 0)) > 0){
if(fwrite(buffer, sizeof(char), length, fp) < length){
printf("File:\t%s Write Failed\n", file);
break;
}
bzero(buffer, BUFFER_SIZE);
}

// 接收成功后,关闭文件
printf("Receive File:\t%s From server IP successfully!\n", file);

fclose(fp);
// 是否继续使用
printf("Continue to use?(Y/N)\n");
char ans[3];
scanf("%s", ans);
if(strcmp(ans,"Y") == 0 || strcmp(ans,"y") == 0)
continue;
else if(strcmp(ans,"N") == 0 || strcmp(ans,"n") == 0)
break;
}
}
else if(strcmp(service_num, "3") == 0){
printf("Quit.");
close(client_socket_fd);
}
// 在前面使用结束,跳出循环后,关闭socket
close(client_socket_fd);
return 0;
}

运行测试

服务端运行在虚拟机的Kali Linux中,客户端运行在本机的mac os上面。先启动服务端,服务端处于监听状态。

再启动客户端,这时即可与服务端进行连接,同时客户端显示可选择的服务。

收发传输

先输入1,选择“传输消息”服务。在终端输入信息,即可传送给服务端。服务端接收到消息,也可在终端打印。当不想进行通信时,输入“quit”,即可退出。服务端可以继续保持在线,等待其他客户端接入,选择不同的服务。

传输文件

再运行一个客户端,选择“传输文件”服务。在终端输入文件名传送到服务端,当接收到文件之后,可以选择继续获取文件或者是终止连接。

服务端可以根据文件名查找文件,并将文件传输过去。

之后就可以在“网络通信”文件夹中找到传输过来的图片。

需要说明的是,当客户端只给出文件名时,服务端是只会在server程序所在文件夹内寻找文件的,并不在整个计算机范围内寻找。并且有许多没有考虑到的地方,如果操作不当,可能造成程序崩溃退出。

参考:

[1] Linux网络编程:socket文件传输范例