1.流程图

2.TCP编程API
-
socket函数
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:创建套接字
参数:
参数1:地址族(Address Family),即IP地址类型,常用有:
AF_INET(IPV4类型),AF_INET6(IPV6类型)
AF_UNIX, AF_LOCAL(本地进程通信)
AF_NETLINK(内核与用户空间的通信,用于设备驱动)
AF_PACKET(原始套接字)
参数2:套接字类型,常用有:
SOCK_STREAM(流式套接字,对应TCP)
SOCK_DGRAM(数据报套接字,对应UDP)
SOCK_RAW(原始套接字)
参数3:TCP与UDP编程时候填0,原始套接字时候需填充
返回值:成功返回一个文件描述符,失败返回-1
*/
int socket(int domain, int type, int protocol);
-
bind函数
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:将一本地地址与一套接口捆绑
参数:
参数1:通过socket函数获得的文件描述符
参数2:结构体地址,基于Internet通信时候填sockaddr_in类型结构体,需要强制转换为sockaddr类型
参数3:结构体长度
返回值:成功返回0,失败返回-1
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr结构体内部为:
struct sockaddr
{
sa_family_t sa_family;/*2字节*/
char sa_data[14];/*14字节*/
};
基于Internet通信时候参数2应该填充的结构体:sockaddr_in结构体,具体信息可查看man手册man 7 ip或者man 7 ipv6:
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
struct sockaddr_in
{
sa_family_t sin_family; /* 2字节,地址族 */
in_port_t sin_port; /* 2字节,基于网络字节序的端口号*/
struct in_addr sin_addr; /* internet address */
unsigned char sin_zer[32]; /*填充字节,一般需要清零*/
};
struct in_addr
{
uint32_t s_addr; /*32位网络字节序IP地址 */
};
-
listen函数
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求
参数:
参数1:通过socket函数获得的文件描述符
参数2:同时允许几路客户端和服务器进行正在连接的过程(正在3次握手),一般写5,ARM最大为8
返回值:成功返回0,失败返回-1
*/
int listen(int sockfd, int backlog);
注意:内核中服务器的套接字fd会维护2个链表,一个是正在进行3次握手的客户端链表(数量 = 2*backlog+1),一个是已经建立好连接的客户端链表(已经完成了3次握手分配好了新的文件描述符)
-
accept函数,man 2 accept查看帮助文档
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:阻塞等待客户端的连接请求
参数:
参数1:通过socket函数获得的文件描述符
参数2:指向sockaddr_in结构体的指针,即保留建立连接的客户端的信息,包含客户端IP地址与端口号
参数3:sizeof(struct sockaddr_in)
返回值:成功返回建立了连接的套接字文件描述符,失败返回-1
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
connect函数
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:
参数:连接服务器
参数1:文件描述符
参数2:同bind函数参数
参数3:参数2长度
返回值:成功返回0,失败返回-1
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
send函数
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:发送数据
参数:
参数1:文件描述符
参数2:发送缓冲区首地址
参数3:发送的字节数
参数4:发送方式,通常为0(与write作用相同)
MSG_DONTWAIT(非阻塞发送)
MSG_OOB(用于发送TCP类型带外数据)
返回值:成功返回实际发送的字节数,失败返回-1
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv函数
#include <sys/types.h>
#include <sys/socket.h>
/*
功能:接收数据
参数:
参数1:文件描述符
参数2:接收缓冲区首地址
参数3:接收的字节数
参数4:接收方式,通常为0(与read作用相同)
MSG_DONTWAIT(非阻塞接收)
MSG_OOB(用于读取TCP类型带外数据)
MSG_PEEK(用于预先读取数据,判断数据是否完整,该操作只是读取内核缓冲区中数据,但
缓冲区内数据并没消失)
返回值:成功返回实际接收的字节数,失败返回-1
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
3.示例
3.1编写程序基于C/S模式,实现客户端与服务器的通信
- 头文件
#ifndef __TCPC1_H__
#define __TCPC1_H__
/*网络相关头文件 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#define SERV_PORT 5001
#define SERV "0.0.0.0"
#define SERV_IP_ADDR "47.105.70.108"
#define BACKLOG 5
#define QUIT_STR "quit\n"
#endif
- 服务器端程序
#include "TCPC1.h"
int main(void)
{
char buf[BUFSIZ];
int fd = -1;
int newfd = -1;
int ret = -1;/*read函数返回值 */
struct sockaddr_in sin;
/*创建socket */
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(-1);
}
/*结构体成员清零 */
bzero(&sin, sizeof(sin));
/*填充结构体 */
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);/*网络字节序端口号 */
#if 0
sin.sin_addr.s_addr = inet_addr(SERV);
#else
if(inet_pton(AF_INET, SERV, (void *)&sin.sin_addr) != 1)
{
perror("inet_pton");
exit(-1);
}
#endif
/*绑定socket */
if(bind(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
/*主动套接字转被动套接字 */
if(listen(fd, BACKLOG) < 0)
{
perror("listen");
exit(-1);
}
/*阻塞等待客户端连接请求 */
newfd = accept(fd, NULL, NULL);
if(newfd < 0)
{
perror("accept");
exit(-1);
}
while(1)
{
/*与newfd进行读写 */
bzero(buf, BUFSIZ);
/*判断读函数是否出错 */
do
{
ret = read(newfd, buf, BUFSIZ - 1);
} while (ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("read");
exit(-1);
}
if(0 == ret)
{
break;
}
printf("Service receive data: %s\n", buf);
if(strcmp(buf, QUIT_STR) == 0)
{
printf("Client is exting...\n");
break;
}
}
close(newfd);
close(fd);
return 0;
}
- 客户端程序
#include "TCPC1.h"
int main(void)
{
int fd = -1;
struct sockaddr_in sin;
char buff[BUFSIZ];
int ret = -1/*write函数返回值 */;
/*创建socket */
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(-1);
}
/*结构体成员清零 */
bzero(&sin, sizeof(sin));
/*填充结构体 */
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);/*网络字节序端口号 */
#if 0
sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);
#else
if(inet_pton(AF_INET, SERV_IP_ADDR, (void *)&sin.sin_addr.s_addr) != 1)
{
perror("inet_pton");
exit(-1);
}
#endif
/*连接服务器 */
if(connect(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("connect");
exit(-1);
}
/*读写数据 */
while(1)
{
bzero(buff, BUFSIZ);
if(fgets(buff, BUFSIZ - 1, stdin) == NULL)
{
continue;
}
write(fd, buff, strlen(buff));
if(strcmp(buff, QUIT_STR) == 0)
{
printf("Client is exting...\n");
break;
}
}
/*关闭套接字 */
close(fd);
return 0;
}
- 效果:将编译生成的server可执行文件放到阿里云服务器上允许,本地允许客户端程序,结果如下:
云端:
[root@HQ InternetTest] ./server
Service receive data: a
Service receive data: quit
Client is exting...
本地端:
hanqi@hanqi-PC:~/C/Internet$ ./client
a
quit
Client is exting...
- 注意:
在服务器端的IP地址应该写为sin.sin_addr.s_addr = htonl(INADDR_ANY),这样可以让任意客户端访问服务器 - 不足:
在上述程序中存在缺陷,可以看到,在服务器端存在两个地方的阻塞,一个是accept()函数会阻塞等待客户端连接,一个是read()函数也会阻塞,并且其位于while(1)之下,所以会一直等待客户端1的输入数据,因此此服务器只能响应一次客户端的连接,即只能连接一个客户端,与实际服务器情况不符,故需改进. - 改进方案:
1.使用多路复用方案解决2.创建多进程/线程为客户端服务
3.2 3.1PLUS
- TCP多进程并发服务器
#include "common.h"
void client_fork(void *arg);
/*子进程结束信号处理函数 */
void sig_child_handle(int signo)
{
/*回收子进程 */
if(SIGCHLD == signo)
{
waitpid(-1, NULL, WNOHANG);
}
}
int main(void)
{
pid_t pid;
char clientIPV4[16];/*客户端IP地址 */
int fd = -1;
int newfd = -1;
int b_reuse = 1;
struct sockaddr_in sin;
struct sockaddr_in client_message;
socklen_t client_message_len = sizeof(client_message);
signal(SIGCHLD, sig_child_handle);
/*创建socket */
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(-1);
}
/*允许绑定地址快速重用 */
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
/*结构体成员清零 */
bzero(&sin, sizeof(sin));
/*填充结构体 */
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);/*网络字节序端口号 */
/*让服务器能收到任意客户端数据 */
sin.sin_addr.s_addr = htonl(INADDR_ANY);
/*绑定socket */
if(bind(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
/*主动套接字转被动套接字 */
if(listen(fd, BACKLOG) < 0)
{
perror("listen");
exit(-1);
}
puts("Server staring ok");
while(1)
{
/*阻塞等待客户端连接请求并保存客户端信息 */
if((newfd = accept(fd, (struct sockaddr *)&client_message, &client_message_len)) < 0)
{
perror("accept");
break;
}
/*创建子进程,用于处理客户端数据 */
if((pid = fork()) < 0)
{
perror("fork");
break;
}
/*子进程 */
if(0 == pid)
{
close(fd);
/*客户端IP地址网络字节序转本地字节序 */
if(!inet_ntop(AF_INET, (void *)&client_message.sin_addr.s_addr, clientIPV4, sizeof(client_message)))
{
perror("inet_ntop");
exit(-1);
}
printf("client(%s: %d) is connected!\n", clientIPV4, ntohs(client_message.sin_port));
client_fork(&newfd);
return 0;
}
/*父进程 */
else
{
close(newfd);
}
}
close(fd);
return 0;
}
void client_fork(void * arg)
{
char buf[BUFSIZ];
int ret = -1;/*read函数返回值 */
int newfd = *(int *)arg;
printf("process newfd = %d\n", newfd);
while(1)
{
/*与newfd进行读写 */
bzero(buf, BUFSIZ);
/*判断读函数是否出错 */
do
{
ret = read(newfd, buf, BUFSIZ - 1);
} while (ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("read");
exit(-1);
}
if(0 == ret)
{
break;
}
printf("Service receive data: %s\n", buf);
if(strcmp(buf, QUIT_STR) == 0)
{
printf("Client(fd = %d) is exting...\n", newfd);
break;
}
}
close(newfd);
}
- TCP多线程并发服务器
#include "common.h"
void *client_thread(void *arg);
int main(void)
{
pthread_t a_thread;
char clientIPV4[16];/*客户端IP地址 */
int fd = -1;
int newfd = -1;
int b_reuse = 1;
struct sockaddr_in sin;
struct sockaddr_in client_message;
socklen_t client_message_len = sizeof(client_message);
/*创建socket */
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(-1);
}
/*允许绑定地址快速重用 */
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
/*结构体成员清零 */
bzero(&sin, sizeof(sin));
/*填充结构体 */
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);/*网络字节序端口号 */
/*让服务器能收到任意客户端数据 */
sin.sin_addr.s_addr = htonl(INADDR_ANY);
/*绑定socket */
if(bind(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
/*主动套接字转被动套接字 */
if(listen(fd, BACKLOG) < 0)
{
perror("listen");
exit(-1);
}
puts("Server staring ok");
while(1)
{
/*阻塞等待客户端连接请求并保存客户端信息 */
if((newfd = accept(fd, (struct sockaddr *)&client_message, &client_message_len)) < 0)
{
perror("accept");
exit(-1);
}
/*客户端IP地址网络字节序转本地字节序 */
if(!inet_ntop(AF_INET, (void *)&client_message.sin_addr.s_addr, clientIPV4, sizeof(client_message)))
{
perror("inet_ntop");
exit(-1);
}
printf("client(%s: %d) is connected!\n", clientIPV4, ntohs(client_message.sin_port));
pthread_create(&a_thread, NULL, client_thread, (void *)&newfd);
}
close(fd);
return 0;
}
void *client_thread(void * arg)
{
char buf[BUFSIZ];
int ret = -1;/*read函数返回值 */
int newfd = *(int *)arg;
printf("thread newfd = %d\n", newfd);
while(1)
{
/*与newfd进行读写 */
bzero(buf, BUFSIZ);
/*判断读函数是否出错 */
do
{
ret = read(newfd, buf, BUFSIZ - 1);
} while (ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("read");
exit(-1);
}
if(0 == ret)
{
break;
}
printf("Service receive data: %s\n", buf);
if(strcmp(buf, QUIT_STR) == 0)
{
printf("Client(fd = %d) is exting...\n", newfd);
break;
}
}
close(newfd);
}
- 通用头文件
#ifndef __TCPC1_H__
#define __TCPC1_H__
/*网络相关头文件 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#define SERV_PORT 5001
#define SERV "0.0.0.0"
#define SERV_IP_ADDR "47.105.70.108"
#define BACKLOG 5
#define QUIT_STR "quit\n"
#endif