我想知道用 C 编程语言打开套接字并将数据写入套接字以进行网络编程的最简单和最有效的方法。
6 回答
你是对的,在 C 中使用套接字有一个困难的语法。相比之下,Java 和 Python 等后来的语言使它变得轻而易举。我发现在 C 中进行套接字编程的最佳教程是Beej 的网络编程指南。我建议您从头开始以获得良好的概述,但如果您现在只需要让一些代码工作,您可以跳到标题为“客户端-服务器背景”的部分。
祝你好运!
您没有提及您在哪个平台上,但 Stevens 的Unix Network Programming副本将是您书架上的一个很好的补充。大多数操作系统使用套接字、绑定、连接等来实现伯克利套接字。
POSIX 7 最小可运行客户端服务器 TCP 示例
在 LAN 中安装两台计算机,例如您的家庭 WiFi 网络。
在一台计算机上运行服务器:
./server.out
获取服务器计算机的 IP ifconfig
,例如192.168.0.10
。
在另一台计算机上,运行:
./client.out 192.168.0.10
现在在客户端键入行,服务器将返回它们以 1 递增(ROT-1 密码)。
服务器.c
#define _XOPEN_SOURCE 700
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv) {
char buffer[BUFSIZ];
char protoname[] = "tcp";
struct protoent *protoent;
int enable = 1;
int i;
int newline_found = 0;
int server_sockfd, client_sockfd;
socklen_t client_len;
ssize_t nbytes_read;
struct sockaddr_in client_address, server_address;
unsigned short server_port = 12345u;
if (argc > 1) {
server_port = strtol(argv[1], NULL, 10);
}
protoent = getprotobyname(protoname);
if (protoent == NULL) {
perror("getprotobyname");
exit(EXIT_FAILURE);
}
server_sockfd = socket(
AF_INET,
SOCK_STREAM,
protoent->p_proto
/* 0 */
);
if (server_sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) {
perror("setsockopt(SO_REUSEADDR) failed");
exit(EXIT_FAILURE);
}
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(server_port);
if (bind(
server_sockfd,
(struct sockaddr*)&server_address,
sizeof(server_address)
) == -1
) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(server_sockfd, 5) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
fprintf(stderr, "listening on port %d\n", server_port);
while (1) {
client_len = sizeof(client_address);
client_sockfd = accept(
server_sockfd,
(struct sockaddr*)&client_address,
&client_len
);
while ((nbytes_read = read(client_sockfd, buffer, BUFSIZ)) > 0) {
printf("received:\n");
write(STDOUT_FILENO, buffer, nbytes_read);
if (buffer[nbytes_read - 1] == '\n')
newline_found;
for (i = 0; i < nbytes_read - 1; i++)
buffer[i]++;
write(client_sockfd, buffer, nbytes_read);
if (newline_found)
break;
}
close(client_sockfd);
}
return EXIT_SUCCESS;
}
客户端.c
#define _XOPEN_SOURCE 700
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv) {
char buffer[BUFSIZ];
char protoname[] = "tcp";
struct protoent *protoent;
char *server_hostname = "127.0.0.1";
char *user_input = NULL;
in_addr_t in_addr;
in_addr_t server_addr;
int sockfd;
size_t getline_buffer = 0;
ssize_t nbytes_read, i, user_input_len;
struct hostent *hostent;
/* This is the struct used by INet addresses. */
struct sockaddr_in sockaddr_in;
unsigned short server_port = 12345;
if (argc > 1) {
server_hostname = argv[1];
if (argc > 2) {
server_port = strtol(argv[2], NULL, 10);
}
}
/* Get socket. */
protoent = getprotobyname(protoname);
if (protoent == NULL) {
perror("getprotobyname");
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, protoent->p_proto);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* Prepare sockaddr_in. */
hostent = gethostbyname(server_hostname);
if (hostent == NULL) {
fprintf(stderr, "error: gethostbyname(\"%s\")\n", server_hostname);
exit(EXIT_FAILURE);
}
in_addr = inet_addr(inet_ntoa(*(struct in_addr*)*(hostent->h_addr_list)));
if (in_addr == (in_addr_t)-1) {
fprintf(stderr, "error: inet_addr(\"%s\")\n", *(hostent->h_addr_list));
exit(EXIT_FAILURE);
}
sockaddr_in.sin_addr.s_addr = in_addr;
sockaddr_in.sin_family = AF_INET;
sockaddr_in.sin_port = htons(server_port);
/* Do the actual connection. */
if (connect(sockfd, (struct sockaddr*)&sockaddr_in, sizeof(sockaddr_in)) == -1) {
perror("connect");
return EXIT_FAILURE;
}
while (1) {
fprintf(stderr, "enter string (empty to quit):\n");
user_input_len = getline(&user_input, &getline_buffer, stdin);
if (user_input_len == -1) {
perror("getline");
exit(EXIT_FAILURE);
}
if (user_input_len == 1) {
close(sockfd);
break;
}
if (write(sockfd, user_input, user_input_len) == -1) {
perror("write");
exit(EXIT_FAILURE);
}
while ((nbytes_read = read(sockfd, buffer, BUFSIZ)) > 0) {
write(STDOUT_FILENO, buffer, nbytes_read);
if (buffer[nbytes_read - 1] == '\n') {
fflush(stdout);
break;
}
}
}
free(user_input);
exit(EXIT_SUCCESS);
}
在带有 Makefile 的 GitHub 上。在 Ubuntu 15.10 上测试。
消息长度
客户端和服务器上的read
调用都在 while 循环中运行。
就像从文件中读取一样,操作系统可能会任意拆分消息以加快处理速度,例如,一个数据包可能比另一个数据包更早到达。
因此协议必须指定消息停止位置的约定。常用方法包括:
- 带有长度指示符的标头(例如 HTTP
Content-Length
) - 终止消息的唯一字符串。这里我们使用
\n
. - 服务器关闭连接:HTTP 允许https://stackoverflow.com/a/25586633/895245。当然有限,因为下一条消息需要重新连接。
下一步
这个例子是有限的,因为:
- 服务器一次只能处理一个客户端连接
- 通讯简单同步。例如:在 P2P 聊天应用程序上,服务器(其他人)可以随时发送消息。
解决这些问题需要线程和可能的其他调用,例如poll
.
从基本套接字读取和写入并不比读取和写入普通文件更难(只需使用 recv 代替 read,如果 write 则使用 send 代替)。当您需要打开套接字时,事情会变得有些棘手。其原因是因为使用套接字(TCP、UDP 等)进行通信的方式有很多种。
我通常用 C++ 编写,但你可以在我写的“如何避免十大套接字编程错误”的白皮书中找到一些用处 - 忽略使用 ACE 工具包的建议(因为它需要 C++),但要注意套接字论文中的错误——它们很容易犯,也很难找到,尤其是对于初学者。 http://www.riverrace.com/sockets10.htm