Linux下基本TCP socket编程之服务器端
首先,在编程之前,需了解网络上两台主机通过TCP通信的基本原理,建立连接时的三次握手及断开连接时的四次握手,在此便不再叙述。
下面,先列出服务器端的代码,再逐一进行分析。
1 #include<stdio.h>
2 #include<strings.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/socket.h>
6 #include<netinet/in.h>
7 #include<arpa/inet.h>
8
9 #define PORT 1234 //端口号
10 #define BACKLOG 1 //请求队列中的最大连接数
11
12 int main()
13 {
14 int listenfd, connectfd; //监听套接字描述符与连接套接字描述符
15 struct sockaddr_in server, client; //服务器端和客户端IPV4地址信息
16 socklen_t addrlen;
17
18 //使用socket()函数,产生TCP套接字
19 if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
20 {
21 perror("socket() error.");
22 exit(1);
23 }
24
25 int opt = 1;
26 //设置套接字选项,使用SO_REUSEADDR允许套接口和一个已经在使用中的地址绑定
27 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
28 //初始化server套接字地址结构
29 bzero(&server, sizeof(server));
30 server.sin_family = AF_INET;
31 server.sin_port = htons(PORT);
32 server.sin_addr.s_addr = htonl(INADDR_ANY);
33
34 //使用bind()函数,将套接字与指定的协议地址绑定
35 if(bind(listenfd, (struct sockaddr *)&server, sizeof(server)) == -1)
36 {
37 perror("Bind() error");
38 exit(1);
39 }
40
41 //使用listen()函数,等待客户端的连接
42 if(listen(listenfd, BACKLOG) == -1)
43 {
44 perror("listen() error.\n");
45 exit(1);
46 }
47
48 addrlen = sizeof(client);
49
50
51 //不断监听客户端的请求
52 while(1)
53 {
54 //接受客户端的连接,客户端的地址信息放在client地址结构中
55 if((connectfd = accept(listenfd, (struct sockaddr *)&client, &addrlen)) == -1)
56 {
57 perror("accept() error\n");
58 exit(1);
59 }
60
61 //使用inet_ntoa()将网络字节序的二进制IP地址转换成相应的点分十进制IP地址
62 //使用ntohs()将16位的短整型数从网络字节序转换成主机字节序
63 printf("You got a connection from client's IP is %s, port is %d\n",
64 inet_ntoa(client.sin_addr), ntohs(client.sin_port));
65 //服务器端对连接的客户端发送一条信息
66 send(connectfd, "Welcome\n", 8, 0);
67 //关闭与客户端的连接
68 close(connectfd);
69 }
70
71 close(listenfd);
72 return 0;
73 }
下面具体分析下服务器端的建立连接的过程以及用到的几个重要函数。
TCP套接字编程中,服务器端实现的步骤如下:
(1)使用socket()函数创建套接字。
(2)将创建的套接字绑定到指定的地址结构。
(3)listen()函数设置套接字为监听模式,使服务器进入被动打开的状态。
(4)接受客户端的连接请求,建立连接。
(5)接收、应答客户端的数据请求。
(6)终止连接。
几个重要的函数:
(1)socket()函数如下:
#include<sys/socket.h>
int socket(int family, int type, int protocol);
family参数指明了协议族,它的值通常为:AF_INET(IPv4协议)、AF_INET6(IPv6协议)、AF_ROUTE(路由套接口)。type参数指明产生套接字的类型,它的值通常为:SOCK_STREAM(TCP使用这种形式)、SOCK_DGRAM(UDP使用这种形式)、SOCK_RAW(原始套接口)。protocol参数是协议标志,一般在调用socket()函数时将其置为0。
(2)bind()函数如下:
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *server, socklen_len addrlen);
绑定函数的作用是为调用socket()函数产生的套接字分配一个本地协议地址,建立地址与套接字的对应关系。注意:协议地址addr是通用地址。sockfd参数是之前调用socket()函数返回的套接字描述符。server参数为一个结构体,其中包含了服务器端的地址信息。因此,必须在调用bind()函数之前,初始化该结构体,指定服务器端绑定的端口号以及IP地址。sockaddr结构体定义在头文件<netinet/in.h>中。其中,IPv4套接字地址结构如下:
typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;
typedef unsigned short sa_family_t;
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
uint8_t sin_len;
sa_family_t sin_family; //协议族类型
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IP地址
char sin_zero[8]; //保留使用
};
注意,server中相关字段在初始化时,必须是网络字节序。addrlen参数是该地址结构(第二个参数)的长度。
(3)listen()函数如下:
#include<sys/socket.h>
int listen(int sockfd, int backlog);
该函数比较简单,调用listen()函数后,将未连接的套接字转换成被动套接字,使它处在监听模式下,服务器的状态从CLOSED转换到了LISTEN状态。
(4)accept()函数如下:
#include<sys/socket.h>
int accept(int listenfd, struct sockaddr *client, socklen_t *addr_len);
accept()函数使服务器接受客户端的连接请求。该函数最多返回三个值:一个既可能是新套接字也可能是错误指示的整数,一个客户进程的协议地址(由client所指),以及该地址的大小(这后两个参数是值-结果参数);也就是说,服务器可以通过参数client来得到请求连接并获得成功的客户的地址和端口号。在实际使用中,如果服务器端并不想保存客户的地址和端口号,也可以使用accept(listenfd, NULL, NULL)。
至此,服务器与客户端的连接便建立了,接下来便是数据的发送和接收。发送和接收数据可以使用write(),read()/send(),recv()函数。
(5)send()函数如下:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数sockfd为套接字描述符,参数buf指向一个用于发送信息的数据缓冲区,len指明传送数据缓冲区的大小。参数flags是传输控制标志,当值为0时,函数所作的操作与write()相同。
(6)recv()函数如下:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
其用法与send()相近,不再多述。
(7)close()函数如下:
#include<unistd.h>
int close(int sockfd);
close()函数用于关闭套接字。sockfd是要关闭的描述符。
到此,整个TCP连接中服务器端从开启socket到关闭socket便已介绍完。以上介绍的为最基本的连接过程,重点需要掌握TCP建立socket连接的过程以及需要调用的函数。