柳大的Linux讲义·基础篇(4)网络编程基础
柳大的Linux游记·基础篇(4)网络编程基础
- Author: 柳大·Poechant
- Blog:Blog.CSDN.net/Poechant
- Email:zhongchao.usytc#gmail.com (#->@)
- Date:March 11th, 2012
- Copyright © 柳大·Poechant(钟超·Michael)
回顾
闲话
最近很忙,博文写的少。感谢一些博友的理解。
有博友发邮件说《柳大的Linux游记》希望继续写下去,希望了解一些与 socket 入门有关的内容。对此深表惭愧,当时也是应一个博友的来信而开始写这个系列的,但仅写了三篇就没继续了。与 socket 相关的文章,在网络上非常多,socket 编程也是基本功。要我来写的话,写出心意很难,我只希望能写系统一些,所以我想先介绍 socket 的基础,然后说说 select,poll 和 epoll 等 IO 复用技术,可能这样会系统一些,也更实用。
W. Richard Stevens的UNIX Network Programming Volume 1中讲解例子的时候都使用了include "unp.h"
,这是Stevens先生在随书源码中的提供的一个包含了所有 UNIX 网络编程会用到的头文件的的一个头文件。但这样对于不了解 UNIX 网络编程以及 socket 的朋友来说,并不是一个好的学习途径。所以我想看完本文后读Stevens先生的杰出作品更好一些 :)
另外,《JVM深入笔记》的第四篇正在整理,最近确实空闲时间比较少,对此感到很抱歉。我会尽量抽时间多分享一些的。
言归正传,下面还是沿袭我的一贯风格,先以最简单的实例开始。
目录
- 快速开始
- 1.1 TCP C/S
- 1.1.1 TCP Server
- 1.1.2 TCP Client
- 1.2. UCP C/S
- 1.2.1 UDP Server
- 1.2.2 UDP Client
- 1.1 TCP C/S
- TCP 和 UCP 的 Socket 编程对比
- 2.1 Server
- 2.2 Client
- 2.3 所使用的 API 对比
- 裸用 socket 的性能很差
1 快速开始
1.1 TCP C/S
无论你是使用 Windows 还是 UNIX-like 的系统,操作系统提供给应用层的网络编程接口都是 Socket。在 5 层的 TCP/IP 网络结构或者 7 层的 OSI 网络结构中,都有传输层,TCP 和 UDP 协议就是为传输层服务的。而网络层的最常用协议就是 IP(IPv4 或 IPv6)在高层编写程序,就需要用到 TCP 协议和 UDP 协议。其直接使用,就是通过 Socket 来实现的。
先看一段简单的 TCP 通信的 Server 与 Client 例程。
1.1.1 TCP Server
下面是一个 TCP 连接的 Server 实例。
#include <string.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Get Port option
if (argc < 2)
{
fprintf(stderr, "ERROR, no port provided\n");
exit(1);
}
int port_no = atoi(argv[1]);
// Get socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// Bind
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(port_no);
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
// Listen
listen(socket_fd, 5);
while (1) {
// Accept
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);
// Read
char buffer[1024];
bzero(buffer, sizeof(buffer));
read(socket_to_client, buffer, sizeof(buffer) - 1);
printf("Here is the message: %s\n", buffer);
// Write
const char *data = "I got your message.";
write(socket_to_client, data, sizeof(data));
// Close
close(socket_to_client);
}
close(socket_fd);
return 0;
}
上面是 TCP 的 Client 的 Simplest Example。概括起来 Scoket Server 编程有如下几个步骤:
1.1.1.1 获取 Socket Descriptor:
// socket function is included in sys/socket.h
// AF_INET is included in sys/socket.h
// SOCK_STREAM is included in sys/socket.h
socket(AF_INET, SOCK_STREAM, 0);
通过sys/socket.h
中的socket
函数。第一个参数表示使用IPv4 Internet Protocol
,如果是AF_INET6
则表示IPv6 Internet Protocol
,其中AF
表示Address Family
,另外还有PF
表示Protocol Family
。第二个参数表示流传输Socket Stream
,流传输是序列化的、可靠的、双向的、面向连接的,Kernel.org 给出的解释是:“Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.”
另外一个常用的是SOCK_DGRAM
表示Socket Diagram
,是无连接的、不可靠的传输方式,Kernel.org 给出的解释是“Supports datagrams (connectionless, unreliable messages of a fixed maximum length).”
第三个参数表示使用的协议族中的哪个协议。一般来说一个协议族经常只有一个协议,所以长使用“0”。具体参见Kernel.org 给出的解释。
1.1.1.2 绑定地址与端口
首先要创建一个struct sockaddr_in
,并设置地址族、监听的外来地址与本地端口号。如下:
// struct sockaddr_in is inclued in netinet/in.h
// bzero function is included in string.h
// atoi is include in stdlib.h
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]))
然后将第 1 步创建的Socket
与这里创建的地址绑定(实际上直接用的是Socket Descriptor
)。
// struct sockaddr is included in sys/socket.h
// bind function is included in sys/socket.h
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
1.1.1.3 开始监听
// listen function is included in sys/socket.h
listen(socket_fd, 5);
第一个参数不用赘述了,第二个参数是连接队列(connection queue)的最大长度。当Server
进行了Accept
后,在新来的请求就得进入队列等待,如果队列满了,再来的连接就会被拒绝。
1.1.1.4 接受连接
// struct sockaddr_in is included in netinet/in.h
// accept function is included in sys/socket.h
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);
在开始监听socket_fd
后,接收来自该Socket
的连接,将获取到的客户端地址和地址长度写入client_addr
和client_addr_length
中。该accept
在成功接受某连接后会得到该连接的Socket
,并将其Socket Descriptor
返回,就得到了socket_to_client
。
1.1.1.5 接收和发送数据
// bzero function is included in string.h
// read function is included in unistd.h
char buffer[1024];
bzero(buffer, sizeof(buffer));
read(socket_to_client, buffer, sizeof(buffer) - 1);
在接受连接后,就可以从该Socket
读取客户端发送来的数据了,数据读取到char *
的字符串中。发送过程也类似。
// write function is included in unistd.h
const char *data = "Server has received your message.";
write(socket_to_client, data, sizeof(data));
1.1.1.6 关闭Socket
// close function is included in unistd.h
close(socket_fd);
以上就简单解释了 TCP Server 的 Socket 通信过程。简单概括如下:
Create Socket - Bind socket with port - Listen socket - Accept connection - Read/Write - Close
1.1.2 TCP Client
再来看看 Client。以下是例程:
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h> // bcopy
#include <stdlib.h>
int main(int argc, char *argv[])
{
// Get options
if (argc < 3)
{
fprintf(stderr, "Usage: %s <hostname> <port>\n", argv[0]);
exit(1);
}
struct hostent *server_host = gethostbyname(argv[1]);
int server_port = atoi(argv[2]);
// Get socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// Connect
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
bcopy((char *) server_host->h_addr, (char *) &server_addr.sin_addr.s_addr, server_host->h_length);
server_addr.sin_port = htons(server_port);
connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
// Input
printf("Please enter the message: ");
char buffer[1024];
bzero(buffer, sizeof(buffer));
fgets(buffer, sizeof(buffer) - 1, stdin);
// Write
write(socket_fd, buffer, strlen(buffer));
// Read
bzero(buffer, sizeof(buffer));
read(socket_fd, buffer, sizeof(buffer) - 1);
printf("%s\n", buffer);
// Close
close(socket_fd);
return 0;
}
上面是 TCP 的 Client 的 Simplest Example。概括起来 Scoket Client 编程有如下几个步骤:
1.1.2.1 获取 Socket Descriptor:
与 Server 一样。
1.1.2.2 连接服务器
// struct sockaddr_in is included in netinet/in.h
// struct sockaddr is included in sys/socket.h
// bzero is included in string.h
// bcopy is included in string.h
// AF_INET is included in sys/socket.h
// connect is included in sys/socket.h
connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
第一个参数是int
型的Socket Descriptor
,通过socket
函数得到。第二个参数是指向struct sockaddr
地址的指针,第三个参数是该地址的大小。而这个地址是通过struct sockaddr_in
得到的,然后用bzero
位初始化,再赋初值,包括地址族为AF_INET
,地址为struct sockaddr_in
中的h_addr
,这里用到了bcopy
位拷贝函数,最后再赋上端口号htons(int server_port)
。connect
的作用,就是将本地的Socket
与服务器建立连接,而这个Socket
则是通过Socket Descriptor
来标示的。
1.1.2.3 发送或接收数据
首先看发送数据:
// write function is included in unistd.h
char buffer[1024]
...
write(socket_fd, buffer, strlen(buffer));
然后用write
函数,向Socket
所连接的服务器发送数据,数据是char *
的字符串。再看下面的接收数据:
// read function is included in unistd.h
read(socket_fd, buffer, sizeof(buffer) - 1);
第一个参数是Socket Descriptor
,第二个参数是char *
的字符串,长度为第三个参数标示的sizeof(buffer)-1
。功能就是从socket_fd
标示的Socket
所连接的服务器读取数据。
1.1.2.4 关闭Socket
// close function is included in unistd.h
close(socket_fd);
以上就简单解释了客户端的最基本的Socket
通信。概括起来的过程就是:
Create Socket - Connect socket with server - Write/Read - Close
1.2 UDP C/S
刚才介绍了最简单的 TCP C/S 模型,下面看看 UDP C/S 模型。
1.2.1 UDP Server
下面是 UDP 连接的 Server 实例:
#include "sys/socket.h"
#include "netinet/in.h"
#include "string.h"
int main(int argc, char *argv[])
{
// Create Socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// Bind
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]));
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
// As accept
while (1)
{
// Receive
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
// Write
const char *msg_send = "Received a datagram: ";
write(1, msg_send, strlen(msg_send));
write(1, buffer, msg_recv_length);
// Send
const char *msg_send_2 = "Got your message\n";
sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));
}
close(socket_fd);
return 0;
}
UDP Server 的建立主要有以下几步:
1.2.1.1 获取 Socket Descriptor
sock=socket(AF_INET, SOCK_DGRAM, 0);
创建Socket
,获取Socket Descriptor
。
1.2.1.2 绑定地址与端口
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]));
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
将端口和允许的地址与Socket
绑定。
1.2.1.3 接收和发送数据
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
接收数据用的是sys/socket.h
中的recvfrom
函数,第一个参数是Socket Descriptor
,第二个参数是用于存储数据的buffer
,第三个参数是数据存储区域的长度(注,对于数组用sizeof
取到的值是数组长度乘以长度;对于指针用sizeof
取到的值是指针长度,对于32位机是4,对于64位机是8)。第四个参数是标志符,一般设置为0,具体可以查看info recvfrom
。第五个参数用于存储客户端的地址,第六个参数是存储客户端地址的socklen_t
型变量的长度。
用起来也很好记(也可以现查现用),先是套接字,然后是存储区及其大小,接着是标志符,最后是客户端地址及其大小。
const char *msg_send_2 = "Got your message\n";
sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));
发送数据用更多是sys/socket.h
中的sendto
函数,第一个参数Socket Descriptor
,第二和第三个参数是所发送的数据及其大小,然后是标示符(一般为0),最后是客户端地址及其大小。
1.2.1.4 关闭Socket
close(socket_fd);
简单概括一下 UDP Server 的 Socket 编程步骤:
Create Socket - Bind socket with port - Recv/Send - Close
1.2.2 UDP Client
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <netdb.h>
int main(int argc, char *argv[])
{
// Create socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
// Initialize server address
struct hostent *server_host = gethostbyname(argv[1]);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // socket internet family
bcopy((char *)server_host->h_addr, (char *)&server_addr.sin_addr.s_addr, server_host->h_length); // socket internet address
server_addr.sin_port = htons(atoi(argv[2])); // socket internet port
// Send
char buffer[1024];
sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));
// Receive and write
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
const char *hint = "Got an ACK: ";
write(1, hint, strlen(hint));
write(1, buffer, msg_recv_length);
// Close
close(socket_fd);
return 0;
}
1.2.2.1 创建Socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
1.2.2.2 发送和接收数据
sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));
与 UDP Server 的 sendto 一样。注意初始化。
1.2.2.3 关闭Socket
close(socket_fd);
总结一下 UDP Client 编程的几个步骤:
Create Socket - Send/Receive - Close
2 图解 TCP 和 UDP 原理
声明:图片来自此处
上图为 TCP 原理图示 上图为 UDP 原理图示3 TCP 和 UCP 的 Socket 编程对比
3.1 Server
TCP 的过程是:
- Create server socket
- Bind the server socket with client addresses and a local server port
- Listen the server socket
- Accept with blocking, until the connection has established and then get a client socket
- Read and write data from the client socket
- Close the client socket as discontecting
- Close the local server socket
UDP 的过程是:
- Create server socket
- Bind the server socket with client addresses and a local server port
- Receive data and client address through the server socket
- Send data to the client address through the server socket
- Close the local server socket
通过对比,我们可以看到,相同点如下:
- 一开始都要创建 socket
- 接着都要绑定 socket 与本地端口和指定的客户端地址
- 最后都要关闭本地 socket
不过这些相似点似乎没什么价值,还是看看不同点吧。
- TCP 要监听端口,然后阻塞式地等待连接;UDP 则通过自身的循环来不断读取,不阻塞也不建立连接。
- TCP 建立连接后会有一个 client socket,然后通过向这个 socket 的读写实现数据传输;UDP 则直接向客户端地址发送和接收数据。
- 因为 TCP 方式有 client socket,所以完成一次传输后,可以关闭 client socket。当然也可以一直连着不关闭。
可以看到,TCP 和 UDP 的本质区别就是面向连接还是无连接的。因为面向连接,所以要监听到是否有 connection 到来,connection 一旦到来,就阻塞住,然后会有一个 socket 跳出来作为代言。通过对这个 socket 的读写就实现了对 connection 的另一端的客户端的读写。
3.2 Client
TCP 的过程是:
- Create client socket
- Connect the server address and port with the client socket
- After connection is established, read and write data to the client socket
- Close the local socket socket
UDP 的过程是:
- Create client socket
- Send data to the server address through the client socket
- Receive data and the server address throught the client socket
- Close the local client socket
可以看到如下区别:
- TCP 方式要 connect 服务器地址/端口与 socket;UDP 则不需要这个过程。
- TCP 方式在 connection 建立后,通过 client socket 读写数据;而 UDP 方式则直接通过 client socket 向服务器地址发送数据。
3.3 所使用的 API 对比
TCP 方式的 Server 用到:
socket
-
bind
和listen
accept
-
read
和write
close
UDP 方式的 Server 用到:
sokect
bind
-
recvfrom
和sendto
close
TCP 方式的 Client 用到:
socket
connect
-
write
和read
close
UDP 方式的 Client 用到:
socket
-
sendto
和recvfrom
close
4 裸用 socket 的性能很差
是的,这是最传统的网络编程方式:“One traditional way to write network servers is to have the main server block on accept(), waiting for a connection. Once a connection comes in, the server fork()s, the child process handles the connection and the main server is able to service new incoming requests.”
下一篇,我会介绍 IO 复用技术中在 Linux 下常用的 select、poll 和 epoll。
5 参考
- http://www.lowtek.com/sockets/select.html
- http://www.kernel.org/doc/man-pages/online/pages/man7/socket.7.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/listen.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/send.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/sendto.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/recv.2.html
- http://www.kernel.org/doc/man-pages/online/pages/man2/recvfrom.2.html
- http://www.linuxhowtos.org/C_C++/socket.htm
-
Happy Coding, enjoy sharing!
转载请注明来自“柳大的CSDN博客”:Blog.CSDN.net/Poechant
-