1.TCP
下图是基于TCP协议的客户端/服务器程序的一般流程:
TCP协议通讯流程
服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发
回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,
服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,
- 再比如read()返回0就表明收到了FIN段
2.出错处理封装函数
第五篇最后的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c:
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
//在多线程的时候注意不要用到exit函数,因为它是终止进程的
void perr_exit(const char *s)
{
perror(s);
exit(1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ( (n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return n;
}
void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (bind(fd, sa, salen) < 0)
perr_exit("bind error");
}
void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (connect(fd, sa, salen) < 0)
perr_exit("connect error");
}
void Listen(int fd, int backlog)
{
if (listen(fd, backlog) < 0)
perr_exit("listen error");
}
int Socket(int family, int type, int protocol)
{
int n;
if ( (n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
void Close(int fd)
{
if (close(fd) == -1)
perr_exit("close error");
}
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}
/* wrap.h */
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
void Bind(int fd, const struct sockaddr *sa, socklen_t salen);
void Connect(int fd, const struct sockaddr *sa, socklen_t salen);
void Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
void Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
static ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif
-------------Makefile
all:
gcc wrap.c -c
gcc server.c -c
gcc client.c -c
gcc wrap.o server.o -o server
gcc wrap.o client.o -o client
.PHONY:clean
clean:
rm server client *.o
3.高并发服务器
并发服务器开发3.1多进程并发服务器
使用多进程并发服务器时要考虑以下几点:
- 父最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
- 系统内创建进程个数(内存大小相关)
- 进程创建过多是否降低整体服务性能(进程调度)
3.1.1server
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <ctype.h>
#include <signal.h>
#include "wrap.h"
#define SERV_PORT 8000
void do_sig(int num)
{
while (waitpid(0, NULL, WNOHANG) > 0)
;
}
int main(int argc, char *argv[])
{
int lfd, cfd, len, i;
int serv_port = SERV_PORT;
char buf[1024], client_ip[128];
struct sockaddr_in serv_addr, client_addr;
socklen_t client_len;
pid_t pid;
//子进程结束时传出一个信号,用sigaction类注册信号并接受
struct sigaction act;
act.sa_handler = do_sig;//信号处理函数do_sig
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);//注册信号
if (argc == 2)
serv_port = atoi(argv[1]);
lfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons((short)serv_port);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(lfd, 128);
printf("wait for connect...\n");
while (1) {
client_len = sizeof(client_addr);
cfd = Accept(lfd, (struct sockaddr *)&client_addr, &client_len);
printf("client:%s\t%d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(client_addr.sin_port));
pid = fork();
if (pid == 0) {
//in child
Close(lfd);
while (1) {
len = Read(cfd, buf, sizeof(buf));
if (len <= 0)
break;
Write(STDOUT_FILENO, buf, len);
for (i = 0; i < len; ++i)
buf[i] = toupper(buf[i]);
Write(cfd, buf, len);
}
Close(cfd);
return 0;
} else if (pid > 0) {
//in parent
Close(cfd);
} else {
perror("fork");
exit(1);
}
}
Close(lfd);
return 0;
}
3.1.2client
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
int sfd, len;
struct sockaddr_in serv_addr;
char buf[4096];
if (argc < 2) {
printf("./client serv_ip\n");
return 1;
}
sfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &serv_addr.sin_addr.s_addr);
connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while (fgets(buf, sizeof(buf), stdin)) {
write(sfd, buf, strlen(buf));
len = read(sfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
}
return 0;
}
3.2多线程并发服务器
在使用线程模型开发服务器时需考虑以下问题:
- 调整进程内最大文件描述符上限
- 线程如有共享数据,考虑线程同步
- 服务于客户端线程退出时,退出处理。(退出值,分离态)
- 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
3.2.1server
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <ctype.h>
#include <signal.h>
#include "wrap.h"
#define SERV_PORT 8000
void *do_work(void *arg)
{
char buf[1024];
int len, i;
int cfd = (int)arg;
//int pthread_detach(pthread_t phread)
pthread_detach(pthread_self());//把进程编程分离态https://blog.csdn.net/github_33736971/article/details/51457415
//pthread_self();获取自己的pid
while (1) {
len = Read(cfd, buf, sizeof(buf));
if (len <= 0)
break;
Write(STDOUT_FILENO, buf, len);
for (i = 0; i < len; ++i)
buf[i] = toupper(buf[i]);
Write(cfd, buf, len);
}
Close(cfd);
return 0;//只能调return 0 不能调exit 0 不然就是终止进程了
}
int main(int argc, char *argv[])
{
int lfd, cfd;
int serv_port = SERV_PORT;
char client_ip[128];
struct sockaddr_in serv_addr, client_addr;
socklen_t client_len;
pthread_t tid;
if (argc == 2)
serv_port = atoi(argv[1]);
lfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons((short)serv_port);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(lfd, 128);
printf("wait for connect...\n");
while (1) {
client_len = sizeof(client_addr);
cfd = Accept(lfd, (struct sockaddr *)&client_addr, &client_len);
printf("client:%s\t%d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
ntohs(client_addr.sin_port));
/*
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数一:线程id,传入传出参数
参数二:线程初始化参数列表 NULL表示默认
参数三:线程任务函数
参数四:参数三的传入传出参数,需要强转(void*)
*/
pthread_create(&tid, NULL, do_work, (void *)cfd);
}
return 0;
}
3.2.2client
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
int sfd, len;
struct sockaddr_in serv_addr;
char buf[4096];
if (argc < 2) {
printf("./client serv_ip\n");
return 1;
}
sfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &serv_addr.sin_addr.s_addr);
connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while (fgets(buf, sizeof(buf), stdin)) {
write(sfd, buf, strlen(buf));
len = read(sfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
}
return 0;
}
3.2.3Makefile
all:
gcc wrap.c -c #多个二进制文件连接成一个app
gcc server.c -c #首先变成默认的.o文件 再多文件连接
gcc client.c -c
gcc wrap.o server.o -lpthread -o server #连接线程库,直接在连接处加上-lphread
gcc wrap.o client.o -o client
.PHONY:clean
clean:
rm server client *.o
网友评论