编写第一个TCP服务器:实现一对一的连接通信
- 一、前言
- 二、需要使用到的API
- 2.1、socket()函数
- 2.2、bind()函数
- 2.3、listen()函数
- 2.4、accept()函数
- 2.5、recv()函数
- 2.6、send()函数
- 2.7、strerror()函数
- 三、实现步骤
- 四、完整代码
- 五、TCP客户端
- 5.1、自己实现一个TCP客户端
- 5.2、Windows下可以使用NetAssist的网络助手工具
- 小结
一、前言
手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。
为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。
二、需要使用到的API
2.1、socket()函数
函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
这个函数建立一个协议族、协议类型、协议编号的socket文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1并设置了errno。
domain参数值含义:
名称 | 含义 |
---|---|
PF_UNIX,PF_LOCAL | 本地通信 |
AF_INET,PF_INET | IPv4协议 |
PF_INET6 | IPv6协议 |
PF_NETLINK | 内核用户界面设备 |
PF_PACKET | 底层包访问 |
type参数值含义:
名称 | 含义 |
---|---|
SOCK_STREAM | TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM | UDP连接 |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出 |
:SOCK_PACKET | 专用类型 |
SOCK_RDM | 提供可靠的数据报文,不保证数据有序 |
SOCK_RAW | 提供原始网络协议访问 |
protocol参数含义:
通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;如果协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
2.2、bind()函数
函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
参数说明:
- 第1个参数sockfd是用socket()函数创建的文件描述符。
- 第2个参数my_addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。
- 第3个参数addrlen是my_addr结构的长度,可以设置成sizeof(struct sockaddr)。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败并设置了errno。
2.3、listen()函数
函数原型:
#include<sys/socket.h>
int listen(int sockfd, int backlog);
参数说明:
- 第1个参数sockfd是用socket()函数创建的文件描述符。
- 第2个参数backlog规定了内核应该为相应套接字排队的最大连接个数。
2.4、accept()函数
函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
参数说明:
- sockefd:套接字描述符,该套接字在listen() 后监听连接。
- addr:(可选)指针。指向一个缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
- addrlen:(可选)指针。输入参数,配合addr一起使用,指向存有addr地址长度的整形数。
2.5、recv()函数
函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int recv( int fd, char *buf, int len, int flags);
参数说明:
- 第一个参数指定接收端套接字描述符;
- 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
- 第三个参数指明buf的长度;
- 第四个参数一般置0。
返回值:
- 返回大于0的数,表示介绍到的数据大小。
- 返回0,表示连接断开。
- 返回-1,表示接受数据错误。
2.6、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:要发送的数据的字节
- int flags:设置为MSG_DONTWAITMSG 时 表示非阻塞,设置为0时 功能和write一样。
返回值:成功返回实际发送的字节数,失败返回 -1并设置了errno。
2.7、strerror()函数
strerror()
函数返回一个指向字符串的指针,该字符串描述参数errnum中传递的错误代码,可能使用当前语言环境的LC_MESSAGES部分来选择适当的语言。(例如,如果errnum为EINVAL,则返回的描述将为“无效参数”。)应用程序不能修改此字符串,但可以通过随后调用strerror()
或strerror_l()
来修改。任何其他库函数,包括perror()
,都不会修改此字符串。
函数原型:
#include <string.h>
char *strerror(int errnum);
int strerror_r(int errnum, char *buf, size_t buflen);
/* XSI-compliant */
char *strerror_r(int errnum, char *buf, size_t buflen);
/* GNU-specific */
char *strerror_l(int errnum, locale_t locale);
三、实现步骤
一对一服务器设计:
(1)创建socket。
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
(2)绑定地址。
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
(3)设置监听。
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
(4)接收连接。
struct sockaddr_in client;
memset(&client,0,sizeof(client));
socklen_t len=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if(clientfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_ACCEPT_FAILED;
}
(5)接收数据。
char buf[BUFFER_LENGTH]={0};
ret=recv(clientfd,buf,BUFFER_LENGTH,0);
if(ret==0) {
printf("connection dropped\n");
}
printf("recv --> %s\n",buf);
(6)发送数据。
if(-1==send(clientfd,buf,ret,0))
{
printf("errno = %d, %s\n",errno,strerror(errno));
}
(7)关闭文件描述符。
close(clientfd);
close(listenfd);
四、完整代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#define LISTEN_PORT 8888
#define BLOCK_SIZE 10
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_BIND_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4
};
int main(int argc,char **argv)
{
// 1.
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
// 2.
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
// 3.
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
// 4.
struct sockaddr_in client;
memset(&client,0,sizeof(client));
socklen_t len=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if(clientfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_ACCEPT_FAILED;
}
printf("client fd = %d\n",clientfd);
int ret=1;
while(ret>0){
// 5.
char buf[BUFFER_LENGTH]={0};
ret=recv(clientfd,buf,BUFFER_LENGTH,0);
if(ret==0) {
printf("connection dropped\n");
break;
}
printf("recv --> %s\n",buf);
if(-1==send(clientfd,buf,ret,0))
{
printf("errno = %d, %s\n",errno,strerror(errno));
}
}
close(clientfd);
close(listenfd);
return 0;
}
编译命令:
gcc -o server server.c
五、TCP客户端
5.1、自己实现一个TCP客户端
自己实现一个TCP客户端连接TCP服务器的代码:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_CONN_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4
};
int main(int argc,char** argv)
{
if(argc<3)
{
printf("Please enter the server IP and port.");
return 0;
}
printf("connect to %s, port=%s\n",argv[1],argv[2]);
int connfd=socket(AF_INET,SOCK_STREAM,0);
if(connfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
struct sockaddr_in serv;
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=inet_addr(argv[1]);
serv.sin_port=htons(atoi(argv[2]));
socklen_t len=sizeof(serv);
int rwfd=connect(connfd,(struct sockaddr*)&serv,len);
if(rwfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
close(rwfd);
return SOCKET_CONN_FAILED;
}
int ret=1;
while(ret>0)
{
char buf[BUFFER_LENGTH]={0};
printf("Please enter the string to send:\n");
scanf("%s",buf);
send(connfd,buf,strlen(buf),0);
memset(buf,0,BUFFER_LENGTH);
printf("recv:\n");
ret=recv(connfd,buf,BUFFER_LENGTH,0);
printf("%s\n",buf);
}
close(rwfd);
return 0;
}
编译:
gcc -o client client.c
5.2、Windows下可以使用NetAssist的网络助手工具
下载地址:http://old.tpyboard.com/downloads/NetAssist.exe
小结
至此,我们实现了一个一对一的服务器连接,这阶段的TCP服务器代码只能接收一个客户端接入,如果客户端断开了就会直接退出。重点是掌握开发TCP服务器的基本流程,下一章节将介绍在此基础上进行升级,实现可以接受多个客户端的同时接入。