一、TCP编程框架
TCP网络包括服务器(server)和客户端(client)两种模式。
二、Linux下TCP编程流程
根据系统给出API接口,根据上面流程,很容易写出一个简单的TCP应用程序。
TCP服务模式API使用流程:
创建socket()套接字
绑定bind()套接字和端口
侦听listen()连接
接收accept()客户端连接
关闭close()套接字
TCP客户端模式API使用流程
创建socket()套接字
连接connect()服务器
关闭close()套接字
二、系统API接口详解
1、创建socket()函数
socket用于创建一个套接字描述符,它不仅可以实现远程主机间的通信,现在也越来越多的被用在不同进程间的通信或用户与内核空间的通信等地方。
函数原型:
int socket(int domain, int type, int protocol)
例程1:
int fd;
if((fd = socket(AF_INET, SOCK_DGRAM, 0))<0)
{
perror("socket");
exit(1);
}
执行成功后返回一个新创建的套接字;若有错误发生则返回一个-1,错误代码存入errno中。
domain:用于指定创建套接字所使用的协议族,在头文件<linux/socket.h>中定义。大多情况下会使用AF_INET,值得注意的是AF_INET和PF_INET的数值是一致的。
常见的协议族如下:
AF_UNIX:创建只在本机内进行通信的套接字。
AF_INET:使用IPv4TCP/IP协议。
AF_INET6:使用IPv6 TCP/IP协议。
type:指明套接子通信的类型
对应的参数如下:
SOCK_STREAM:创建TCP流套接字。
SOCK_DGRAM:创建UDP数据报套接字。
SOCK_RAW:创建原始套接字。
protocol:指定某个协议的特定类型。
参数protocol通常设置为0,表示通过参数domain指定的协议族和参数type指定的套接字类型来确定使用的协议。当为原始套接字时,系统无法唯一的确定协议,此时就需要使用使用该参数指定所使用的协议。
2. 绑定bind()
将由socket()函数创建的socket文件描述符和一个本地地址结构体(struct sockaddr)绑定起来。
函数原型:
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen)
例程2:
struct sockaddr_in addr_server, addr_client;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(SERVER_PORT);
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(fd, (struct sockaddr *)&addr_server, sizeof(struct sockaddr_in)) <0 )
{
perror("bind");
exit(1);
}
函数成功后返回0,当有错误发生时则返回-1,错误代码存入errno中。
sockfd:sockfd是调用socket函数返回的文件描述符,也就是例程1中的定义的fd;
my_addr: 是一个指向sockaddr结构的指针,它保存着本地套接字的地址(即端口和IP地址)信息。不过由于系统兼容性的问题,一般不使用这个结构,而使用另外一个结构(structsockaddr_in)来代替。
addrlen:是sockaddr或sockaddr_in结构的长度。
这里着重讲解一下sockaddr和sockaddr_in结构体
struct sockaddr定义了一种通用的套接字地址,它在sys/socket.h 中定义。
struct sockaddr{
unsigned short sa_family; /*地址类型,如AF_INET*/
char sa_data[14]; /*14字节的协议地址*/
}
sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET。
sa_data:存储具体的协议地址。
struct sockaddr_in它在netinet/in.h头文件中定义。
struct sockaddr_in{
unsigned short sin_family; /*地址类型*/
unsigned short sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /*填充字节,一般为0*/
}
sin_family:表示地址类型,对于使用TCP/IP协议进行的网络编程,该值只能是AF_INET.
sin_port:是端口号
sin_addr:用来存储32位的IP地址。
数组sin_zero为填充字段,一般赋值为0.
struct in_addr的定义如下:
struct in_addr{
unsigned long s_addr;
}
结构体sockaddr和sockaddr_in的长度都为为16字节。可以将参数my_addr的sin_addr设置为INADDR_ANY而不是某个确定的IP地址就可以绑定到任何网络接口。对于只有一IP地址的计算机,INADDR_ANY对应的就是它的IP地址;对于多宿主主机(拥有多个网卡),INADDR_ANY表示本服务器程序将处理来自所有网络接口上相应端口的连接请求。
3. 监听本地端口listen()
listen()将sockfd作为被动的连接的监听套接字和设置服务器最大同时处理客户端连接请求等候数。服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接。
原型函数:
int listen(int sockfd, int backlog);
例程3:
if(listen(sock_fd, 5) < 0){
perror("listen");
exit(1);
}
sockfd:sockfd是调用socket函数返回的文件描述符,也就是例程1中的定义的fd;
backlog:指定该连接队列的最大长度。如果连接队列已经达到最大,之后的连接请求被服务器拒绝。大多数系统的设置为20,可以将其设置修改为5或者10,根据系统可承受负载或者应用程序的需求来确定。
4. accept接收连接accept()
客户端主动connect()连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,服务器处理接收请求。
函数accept()成功执行后,会返回一个新的套接口文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据收发。
原型函数:
int accept(int sock_fd, struct sockaddr *addr, socklen_t *addrlen);
例程4:
int client_fd;
int client_len;
struct sockaddr_in client_addr;
client_len = sizeof(struct sockaddr_in);
if((client_fd = accept(sock_fd,(struct sockaddr *)&client_addr, &client_len)) < 0)
{
peeror("accept");
exit(1);
}
sockfd:sockfd是调用socket函数返回的文件描述符,也就是例程1中的定义的fd;
addr:用来保存发起连接请求也就是客户端的主机的地址和端口。
addrlen:是addr 所指向的结构体的大小。
sock_fd所指定的套接字被设置为阻塞方式(Linux下的默认方式),且连接请求队列为空,则accept()将被阻塞直到有连接请求到此为止;如果套接字被设置为非阻塞方式,如果队列为空,accept将立即返回-1,errno被设置为EAGAIN。
5. 主动连接connect()
这里TCP和UDP使用区别的。TCP下客户端在建立套接字之后,利用connect()函数,根据制定的地址和端口号,向服务器发送连接请求。UDP下,则connect函数并不建立真正的连接,它只是告诉内核与该套接字进行通信的目的地址(由第二个参数指定),只有该目的地址发来的数据才会被该socket接收。UDP调用connect函数的好处是不必在每次发送和接收数据时都指定目的地址。
原型函数:
int connect(int sock_fd, struct sockaddr* serv_addr, socklen_t addrlen);
例程6:
if(connect(int sock_fd, (struct sockaddr *)&addr_server, sizeof(struct sockaddr))< 0)
{
perror("connect");
exit(1);
}
sockfd:sockfd是调用socket函数返回的文件描述符,也就是例程1中的定义的fd;
serv_addr:是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的IP地址和端口号。
addrlen:表示了第二了参数的大小,可以使用sizeof(struct sockaddr)。
三、TCP实例
服务器程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
int main()
{
int sock_fd, client_fd;
int addr_client_len;
struct sockaddr_in addr_server, addr_client;
addr_client_len = sizeof(struct sockaddr);
if((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(1);
}
printf("sock successful\n");
memset(&addr_server, 0, sizeof(addr_server));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(PORT);
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock_fd, (struct sockaddr *)&addr_server, sizeof(struct sockaddr_in)) < 0)
{
perror("bind");
exit(1);
}
printf("bind sucessful\n");
if(listen(sock_fd, 5))
{
perror("listen");
exit(1);
}
printf("listen sucessful\n");
while(1)
{
if((client_fd = accept(sock_fd, (struct sockaddr *)&addr_client, &addr_client_len)) < 0)
{
perror("accept");
exit(1);
}
printf("accept client ip: %s\n", inet_ntoa(addr_client.sin_addr));
send(client_fd, "ok", 2, 0);
close(client_fd);
}
close(sock_fd);
}
客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_PORT 8080
#define SERVER_IP "192.168.9.102"
int main()
{
int sock_fd;
struct sockaddr_in addr_server;
if((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(1);
}
printf("sock successful\n");
memset(&addr_server, 0, sizeof(addr_server));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(SERVER_PORT);
addr_server.sin_addr.s_addr = inet_addr(SERVER_IP);
if(connect(sock_fd, (struct sockaddr *)&addr_server, sizeof(struct sockaddr))<0)
{
perror("connect");
exit(1);
}
printf("connect successful\n");
send(sock_fd, "ok", 2, 0);
close(sock_fd);
}